mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-06-11 22:21:07 +00:00
contracts: Don't rely on reserved balances keeping an account alive (#13369)
* Move storage deposits to their own account * Take ed for contract's account from origin * Apply suggestions from code review Co-authored-by: Cyrill Leutwiler <bigcyrill@hotmail.com> Co-authored-by: Sasha Gryaznov <hi@agryaznov.com> * Update stale docs * Use 16 bytes prefix for address derivation * Update frame/contracts/src/address.rs Co-authored-by: Cyrill Leutwiler <bigcyrill@hotmail.com> * Fix merge * ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts * Update frame/contracts/primitives/src/lib.rs Co-authored-by: Sasha Gryaznov <hi@agryaznov.com> --------- Co-authored-by: Cyrill Leutwiler <bigcyrill@hotmail.com> Co-authored-by: Sasha Gryaznov <hi@agryaznov.com> Co-authored-by: command-bot <>
This commit is contained in:
committed by
GitHub
parent
292e5ee4e7
commit
330484bb19
@@ -0,0 +1,81 @@
|
||||
// This file is part of Substrate.
|
||||
|
||||
// Copyright (C) 2018-2022 Parity Technologies (UK) Ltd.
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Functions that deal with address derivation.
|
||||
|
||||
use crate::{CodeHash, Config};
|
||||
use codec::{Decode, Encode};
|
||||
use sp_runtime::traits::{Hash, TrailingZeroInput};
|
||||
|
||||
/// Provides the contract address generation method.
|
||||
///
|
||||
/// See [`DefaultAddressGenerator`] for the default implementation.
|
||||
///
|
||||
/// # Note for implementors
|
||||
///
|
||||
/// 1. Make sure that there are no collisions, different inputs never lead to the same output.
|
||||
/// 2. Make sure that the same inputs lead to the same output.
|
||||
pub trait AddressGenerator<T: Config> {
|
||||
/// The address of a contract based on the given instantiate parameters.
|
||||
///
|
||||
/// Changing the formular for an already deployed chain is fine as long as no collisons
|
||||
/// with the old formular. Changes only affect existing contracts.
|
||||
fn contract_address(
|
||||
deploying_address: &T::AccountId,
|
||||
code_hash: &CodeHash<T>,
|
||||
input_data: &[u8],
|
||||
salt: &[u8],
|
||||
) -> T::AccountId;
|
||||
|
||||
/// The address of the deposit account of `contract_addr`.
|
||||
///
|
||||
/// The address is generated once on instantiation and then stored in the contracts
|
||||
/// metadata. Hence changes do only affect newly created contracts.
|
||||
fn deposit_address(contract_addr: &T::AccountId) -> T::AccountId;
|
||||
}
|
||||
|
||||
/// Default address generator.
|
||||
///
|
||||
/// This is the default address generator used by contract instantiation. Its result
|
||||
/// is only dependent on its inputs. It can therefore be used to reliably predict the
|
||||
/// address of a contract. This is akin to the formula of eth's CREATE2 opcode. There
|
||||
/// is no CREATE equivalent because CREATE2 is strictly more powerful.
|
||||
/// Formula:
|
||||
/// `hash("contract_addr_v1" ++ deploying_address ++ code_hash ++ input_data ++ salt)`
|
||||
pub struct DefaultAddressGenerator;
|
||||
|
||||
impl<T: Config> AddressGenerator<T> for DefaultAddressGenerator {
|
||||
/// Formula: `hash("contract_addr_v1" ++ deploying_address ++ code_hash ++ input_data ++ salt)`
|
||||
fn contract_address(
|
||||
deploying_address: &T::AccountId,
|
||||
code_hash: &CodeHash<T>,
|
||||
input_data: &[u8],
|
||||
salt: &[u8],
|
||||
) -> T::AccountId {
|
||||
let entropy = (b"contract_addr_v1", deploying_address, code_hash, input_data, salt)
|
||||
.using_encoded(T::Hashing::hash);
|
||||
Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
|
||||
.expect("infinite length input; no invalid inputs for type; qed")
|
||||
}
|
||||
|
||||
/// Formula: `hash("contract_depo_v1" ++ contract_addr)`
|
||||
fn deposit_address(contract_addr: &T::AccountId) -> T::AccountId {
|
||||
let entropy = (b"contract_depo_v1", contract_addr).using_encoded(T::Hashing::hash);
|
||||
Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
|
||||
.expect("infinite length input; no invalid inputs for type; qed")
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ use self::{
|
||||
use crate::{
|
||||
exec::{AccountIdOf, FixSizedKey, VarSizedKey},
|
||||
schedule::{API_BENCHMARK_BATCH_SIZE, INSTR_BENCHMARK_BATCH_SIZE},
|
||||
storage::Storage,
|
||||
wasm::CallFlags,
|
||||
Pallet as Contracts, *,
|
||||
};
|
||||
@@ -133,14 +132,8 @@ where
|
||||
fn store(&self, items: &Vec<(FixSizedKey, Vec<u8>)>) -> Result<(), &'static str> {
|
||||
let info = self.info()?;
|
||||
for item in items {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
&item.0 as &FixSizedKey,
|
||||
Some(item.1.clone()),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.map_err(|_| "Failed to write storage to restoration dest")?;
|
||||
info.write(&item.0 as &FixSizedKey, Some(item.1.clone()), None, false)
|
||||
.map_err(|_| "Failed to write storage to restoration dest")?;
|
||||
}
|
||||
<ContractInfoOf<T>>::insert(&self.account_id, info);
|
||||
Ok(())
|
||||
@@ -207,7 +200,7 @@ benchmarks! {
|
||||
// The base weight consumed on processing contracts deletion queue.
|
||||
#[pov_mode = Measured]
|
||||
on_process_deletion_queue_batch {}: {
|
||||
Storage::<T>::process_deletion_queue_batch(Weight::MAX)
|
||||
ContractInfo::<T>::process_deletion_queue_batch(Weight::MAX)
|
||||
}
|
||||
|
||||
#[skip_meta]
|
||||
@@ -215,9 +208,9 @@ benchmarks! {
|
||||
on_initialize_per_trie_key {
|
||||
let k in 0..1024;
|
||||
let instance = Contract::<T>::with_storage(WasmModule::dummy(), k, T::Schedule::get().limits.payload_len)?;
|
||||
Storage::<T>::queue_trie_for_deletion(&instance.info()?)?;
|
||||
instance.info()?.queue_trie_for_deletion()?;
|
||||
}: {
|
||||
Storage::<T>::process_deletion_queue_batch(Weight::MAX)
|
||||
ContractInfo::<T>::process_deletion_queue_batch(Weight::MAX)
|
||||
}
|
||||
|
||||
#[pov_mode = Measured]
|
||||
@@ -225,11 +218,11 @@ benchmarks! {
|
||||
let q in 0..1024.min(T::DeletionQueueDepth::get());
|
||||
for i in 0 .. q {
|
||||
let instance = Contract::<T>::with_index(i, WasmModule::dummy(), vec![])?;
|
||||
Storage::<T>::queue_trie_for_deletion(&instance.info()?)?;
|
||||
instance.info()?.queue_trie_for_deletion()?;
|
||||
ContractInfoOf::<T>::remove(instance.account_id);
|
||||
}
|
||||
}: {
|
||||
Storage::<T>::process_deletion_queue_batch(Weight::MAX)
|
||||
ContractInfo::<T>::process_deletion_queue_batch(Weight::MAX)
|
||||
}
|
||||
|
||||
// This benchmarks the additional weight that is charged when a contract is executed the
|
||||
@@ -293,18 +286,16 @@ benchmarks! {
|
||||
let addr = Contracts::<T>::contract_address(&caller, &hash, &input, &salt);
|
||||
}: _(origin, value, Weight::MAX, None, code, input, salt)
|
||||
verify {
|
||||
// the contract itself does not trigger any reserves
|
||||
let deposit = T::Currency::reserved_balance(&addr);
|
||||
let deposit_account = Contract::<T>::address_info(&addr)?.deposit_account().clone();
|
||||
let deposit = T::Currency::free_balance(&deposit_account);
|
||||
// uploading the code reserves some balance in the callers account
|
||||
let code_deposit = T::Currency::reserved_balance(&caller);
|
||||
assert_eq!(
|
||||
T::Currency::free_balance(&caller),
|
||||
caller_funding::<T>() - value - deposit - code_deposit,
|
||||
caller_funding::<T>() - value - deposit - code_deposit - Pallet::<T>::min_balance(),
|
||||
);
|
||||
// contract has the full value
|
||||
assert_eq!(T::Currency::free_balance(&addr), value);
|
||||
// instantiate should leave a contract
|
||||
Contract::<T>::address_info(&addr)?;
|
||||
assert_eq!(T::Currency::free_balance(&addr), value + Pallet::<T>::min_balance());
|
||||
}
|
||||
|
||||
// Instantiate uses a dummy contract constructor to measure the overhead of the instantiate.
|
||||
@@ -325,14 +316,15 @@ benchmarks! {
|
||||
Contracts::<T>::store_code_raw(code, caller.clone())?;
|
||||
}: _(origin, value, Weight::MAX, None, hash, input, salt)
|
||||
verify {
|
||||
// the contract itself does not trigger any reserves
|
||||
let deposit = T::Currency::reserved_balance(&addr);
|
||||
let deposit_account = Contract::<T>::address_info(&addr)?.deposit_account().clone();
|
||||
let deposit = T::Currency::free_balance(&deposit_account);
|
||||
// value was removed from the caller
|
||||
assert_eq!(T::Currency::free_balance(&caller), caller_funding::<T>() - value - deposit);
|
||||
assert_eq!(
|
||||
T::Currency::free_balance(&caller),
|
||||
caller_funding::<T>() - value - deposit - Pallet::<T>::min_balance(),
|
||||
);
|
||||
// contract has the full value
|
||||
assert_eq!(T::Currency::free_balance(&addr), value);
|
||||
// instantiate should leave a contract
|
||||
Contract::<T>::address_info(&addr)?;
|
||||
assert_eq!(T::Currency::free_balance(&addr), value + Pallet::<T>::min_balance());
|
||||
}
|
||||
|
||||
// We just call a dummy contract to measure the overhead of the call extrinsic.
|
||||
@@ -348,18 +340,19 @@ benchmarks! {
|
||||
let instance = Contract::<T>::with_caller(
|
||||
whitelisted_caller(), WasmModule::dummy(), vec![],
|
||||
)?;
|
||||
let deposit_account = instance.info()?.deposit_account().clone();
|
||||
let value = Pallet::<T>::min_balance();
|
||||
let origin = RawOrigin::Signed(instance.caller.clone());
|
||||
let callee = instance.addr.clone();
|
||||
let before = T::Currency::free_balance(&instance.account_id);
|
||||
let before_deposit = T::Currency::free_balance(&deposit_account);
|
||||
}: _(origin, callee, value, Weight::MAX, None, data)
|
||||
verify {
|
||||
// the contract itself does not trigger any reserves
|
||||
let deposit = T::Currency::reserved_balance(&instance.account_id);
|
||||
let deposit = T::Currency::free_balance(&deposit_account);
|
||||
// value and value transfered via call should be removed from the caller
|
||||
assert_eq!(
|
||||
T::Currency::free_balance(&instance.caller),
|
||||
caller_funding::<T>() - instance.value - value - deposit,
|
||||
caller_funding::<T>() - instance.value - value - deposit - Pallet::<T>::min_balance(),
|
||||
);
|
||||
// contract should have received the value
|
||||
assert_eq!(T::Currency::free_balance(&instance.account_id), before + value);
|
||||
@@ -798,14 +791,16 @@ benchmarks! {
|
||||
});
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let origin = RawOrigin::Signed(instance.caller.clone());
|
||||
let deposit_account = instance.info()?.deposit_account().clone();
|
||||
assert_eq!(T::Currency::total_balance(&beneficiary), 0u32.into());
|
||||
assert_eq!(T::Currency::free_balance(&instance.account_id), Pallet::<T>::min_balance());
|
||||
assert_ne!(T::Currency::reserved_balance(&instance.account_id), 0u32.into());
|
||||
assert_eq!(T::Currency::free_balance(&instance.account_id), Pallet::<T>::min_balance() * 2u32.into());
|
||||
assert_ne!(T::Currency::free_balance(&deposit_account), 0u32.into());
|
||||
}: call(origin, instance.addr.clone(), 0u32.into(), Weight::MAX, None, vec![])
|
||||
verify {
|
||||
if r > 0 {
|
||||
assert_eq!(T::Currency::total_balance(&instance.account_id), 0u32.into());
|
||||
assert_eq!(T::Currency::total_balance(&beneficiary), Pallet::<T>::min_balance());
|
||||
assert_eq!(T::Currency::total_balance(&deposit_account), 0u32.into());
|
||||
assert_eq!(T::Currency::total_balance(&beneficiary), Pallet::<T>::min_balance() * 2u32.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1039,8 +1034,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![]),
|
||||
None,
|
||||
@@ -1088,8 +1082,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![]),
|
||||
None,
|
||||
@@ -1137,8 +1130,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![42u8; (n * 2048) as usize]), // value_len increments by 2kb up to max payload_len
|
||||
None,
|
||||
@@ -1188,8 +1180,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![]),
|
||||
None,
|
||||
@@ -1236,8 +1227,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![42u8; (n * 2048) as usize]), // value_len increments by 2kb up to max payload_len
|
||||
None,
|
||||
@@ -1291,8 +1281,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![]),
|
||||
None,
|
||||
@@ -1346,8 +1335,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![42u8; (n * 2048) as usize]), // value_len increments by 2kb up to max payload_len
|
||||
None,
|
||||
@@ -1396,8 +1384,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![]),
|
||||
None,
|
||||
@@ -1444,8 +1431,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![42u8; (n * 2048) as usize]), // value_len increments by 2kb up to max payload_len
|
||||
None,
|
||||
@@ -1499,8 +1485,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![]),
|
||||
None,
|
||||
@@ -1554,8 +1539,7 @@ benchmarks! {
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
let info = instance.info()?;
|
||||
for key in keys {
|
||||
Storage::<T>::write(
|
||||
&info.trie_id,
|
||||
info.write(
|
||||
&VarSizedKey::<T>::try_from(key).map_err(|e| "Key has wrong length")?,
|
||||
Some(vec![42u8; (n * 2048) as usize]), // value_len increments by 2kb up to max payload_len
|
||||
None,
|
||||
@@ -1884,7 +1868,7 @@ benchmarks! {
|
||||
.. Default::default()
|
||||
});
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
instance.set_balance(value * (r * API_BENCHMARK_BATCH_SIZE + 1).into());
|
||||
instance.set_balance((value + Pallet::<T>::min_balance()) * (r * API_BENCHMARK_BATCH_SIZE + 1).into());
|
||||
let origin = RawOrigin::Signed(instance.caller.clone());
|
||||
let callee = instance.addr.clone();
|
||||
let addresses = hashes
|
||||
@@ -1997,7 +1981,7 @@ benchmarks! {
|
||||
.. Default::default()
|
||||
});
|
||||
let instance = Contract::<T>::new(code, vec![])?;
|
||||
instance.set_balance(value * (API_BENCHMARK_BATCH_SIZE + 1).into());
|
||||
instance.set_balance((value + Pallet::<T>::min_balance()) * (API_BENCHMARK_BATCH_SIZE + 1).into());
|
||||
let origin = RawOrigin::Signed(instance.caller.clone());
|
||||
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
|
||||
|
||||
@@ -3113,8 +3097,8 @@ benchmarks! {
|
||||
{
|
||||
let weight_limit = T::DeletionWeightLimit::get();
|
||||
let max_queue_depth = T::DeletionQueueDepth::get() as usize;
|
||||
let empty_queue_throughput = Storage::<T>::deletion_budget(0, weight_limit);
|
||||
let full_queue_throughput = Storage::<T>::deletion_budget(max_queue_depth, weight_limit);
|
||||
let empty_queue_throughput = ContractInfo::<T>::deletion_budget(0, weight_limit);
|
||||
let full_queue_throughput = ContractInfo::<T>::deletion_budget(max_queue_depth, weight_limit);
|
||||
println!("{:#?}", Schedule::<T>::default());
|
||||
println!("###############################################");
|
||||
println!("Lazy deletion weight per key: {}", empty_queue_throughput.0);
|
||||
|
||||
@@ -17,9 +17,9 @@
|
||||
|
||||
use crate::{
|
||||
gas::GasMeter,
|
||||
storage::{self, Storage, WriteOutcome},
|
||||
storage::{self, DepositAccount, WriteOutcome},
|
||||
BalanceOf, CodeHash, Config, ContractInfo, ContractInfoOf, DebugBufferVec, Determinism, Error,
|
||||
Event, Nonce, Pallet as Contracts, Schedule,
|
||||
Event, Nonce, Pallet as Contracts, Schedule, System,
|
||||
};
|
||||
use frame_support::{
|
||||
crypto::ecdsa::ECDSAExt,
|
||||
@@ -492,7 +492,7 @@ enum CachedContract<T: Config> {
|
||||
///
|
||||
/// In this case a reload is neither allowed nor possible. Please note that recursive
|
||||
/// calls cannot remove a contract as this is checked and denied.
|
||||
Terminated,
|
||||
Terminated(DepositAccount<T>),
|
||||
}
|
||||
|
||||
impl<T: Config> CachedContract<T> {
|
||||
@@ -513,6 +513,15 @@ impl<T: Config> CachedContract<T> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `Some` iff the contract is not `Cached::Invalidated`.
|
||||
fn deposit_account(&self) -> Option<&DepositAccount<T>> {
|
||||
match self {
|
||||
CachedContract::Cached(contract) => Some(contract.deposit_account()),
|
||||
CachedContract::Terminated(deposit_account) => Some(&deposit_account),
|
||||
CachedContract::Invalidated => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Config> Frame<T> {
|
||||
@@ -591,7 +600,9 @@ impl<T: Config> CachedContract<T> {
|
||||
/// Terminate and return the contract info.
|
||||
fn terminate(&mut self, account_id: &T::AccountId) -> ContractInfo<T> {
|
||||
self.load(account_id);
|
||||
get_cached_or_panic_after_load!(mem::replace(self, Self::Terminated))
|
||||
let contract = get_cached_or_panic_after_load!(self);
|
||||
let deposit_account = contract.deposit_account().clone();
|
||||
get_cached_or_panic_after_load!(mem::replace(self, Self::Terminated(deposit_account)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -751,9 +762,7 @@ where
|
||||
input_data,
|
||||
salt,
|
||||
);
|
||||
let trie_id = Storage::<T>::generate_trie_id(&account_id, nonce);
|
||||
let contract =
|
||||
Storage::<T>::new_contract(&account_id, trie_id, *executable.code_hash())?;
|
||||
let contract = ContractInfo::new(&account_id, nonce, *executable.code_hash())?;
|
||||
(
|
||||
account_id,
|
||||
contract,
|
||||
@@ -873,7 +882,7 @@ where
|
||||
match (entry_point, delegated_code_hash) {
|
||||
(ExportedFunction::Constructor, _) => {
|
||||
// It is not allowed to terminate a contract inside its constructor.
|
||||
if matches!(frame.contract_info, CachedContract::Terminated) {
|
||||
if matches!(frame.contract_info, CachedContract::Terminated(_)) {
|
||||
return Err(Error::<T>::TerminatedInConstructor.into())
|
||||
}
|
||||
|
||||
@@ -962,11 +971,21 @@ where
|
||||
|
||||
// Record the storage meter changes of the nested call into the parent meter.
|
||||
// If the dropped frame's contract wasn't terminated we update the deposit counter
|
||||
// in its contract info. The load is necessary to to pull it from storage in case
|
||||
// in its contract info. The load is necessary to pull it from storage in case
|
||||
// it was invalidated.
|
||||
frame.contract_info.load(account_id);
|
||||
let deposit_account = frame
|
||||
.contract_info
|
||||
.deposit_account()
|
||||
.expect(
|
||||
"Is only `None` when the info is invalidated.
|
||||
We just re-loaded from storage which either makes the state `Cached` or `Terminated`.
|
||||
qed",
|
||||
)
|
||||
.clone();
|
||||
let mut contract = frame.contract_info.into_contract();
|
||||
prev.nested_storage.absorb(frame.nested_storage, account_id, contract.as_mut());
|
||||
prev.nested_storage
|
||||
.absorb(frame.nested_storage, deposit_account, contract.as_mut());
|
||||
|
||||
// In case the contract wasn't terminated we need to persist changes made to it.
|
||||
if let Some(contract) = contract {
|
||||
@@ -1001,10 +1020,14 @@ where
|
||||
if !persist {
|
||||
return
|
||||
}
|
||||
let deposit_account = self.first_frame.contract_info.deposit_account().expect(
|
||||
"Is only `None` when the info is invalidated. The first frame can't be invalidated.
|
||||
qed",
|
||||
).clone();
|
||||
let mut contract = self.first_frame.contract_info.as_contract();
|
||||
self.storage_meter.absorb(
|
||||
mem::take(&mut self.first_frame.nested_storage),
|
||||
&self.first_frame.account_id,
|
||||
deposit_account,
|
||||
contract.as_deref_mut(),
|
||||
);
|
||||
if let Some(contract) = contract {
|
||||
@@ -1182,19 +1205,21 @@ where
|
||||
}
|
||||
|
||||
fn terminate(&mut self, beneficiary: &AccountIdOf<Self::T>) -> Result<(), DispatchError> {
|
||||
use frame_support::traits::fungible::Inspect;
|
||||
if self.is_recursive() {
|
||||
return Err(Error::<T>::TerminatedWhileReentrant.into())
|
||||
}
|
||||
let frame = self.top_frame_mut();
|
||||
let info = frame.terminate();
|
||||
frame.nested_storage.terminate(&info);
|
||||
Storage::<T>::queue_trie_for_deletion(&info)?;
|
||||
<Stack<'a, T, E>>::transfer(
|
||||
ExistenceRequirement::AllowDeath,
|
||||
System::<T>::dec_consumers(&frame.account_id);
|
||||
T::Currency::transfer(
|
||||
&frame.account_id,
|
||||
beneficiary,
|
||||
T::Currency::free_balance(&frame.account_id),
|
||||
T::Currency::reducible_balance(&frame.account_id, false),
|
||||
ExistenceRequirement::AllowDeath,
|
||||
)?;
|
||||
info.queue_trie_for_deletion()?;
|
||||
ContractInfoOf::<T>::remove(&frame.account_id);
|
||||
E::remove_user(info.code_hash);
|
||||
Contracts::<T>::deposit_event(
|
||||
@@ -1212,19 +1237,19 @@ where
|
||||
}
|
||||
|
||||
fn get_storage(&mut self, key: &FixSizedKey) -> Option<Vec<u8>> {
|
||||
Storage::<T>::read(&self.top_frame_mut().contract_info().trie_id, key)
|
||||
self.top_frame_mut().contract_info().read(key)
|
||||
}
|
||||
|
||||
fn get_storage_transparent(&mut self, key: &VarSizedKey<T>) -> Option<Vec<u8>> {
|
||||
Storage::<T>::read(&self.top_frame_mut().contract_info().trie_id, key)
|
||||
self.top_frame_mut().contract_info().read(key)
|
||||
}
|
||||
|
||||
fn get_storage_size(&mut self, key: &FixSizedKey) -> Option<u32> {
|
||||
Storage::<T>::size(&self.top_frame_mut().contract_info().trie_id, key)
|
||||
self.top_frame_mut().contract_info().size(key)
|
||||
}
|
||||
|
||||
fn get_storage_size_transparent(&mut self, key: &VarSizedKey<T>) -> Option<u32> {
|
||||
Storage::<T>::size(&self.top_frame_mut().contract_info().trie_id, key)
|
||||
self.top_frame_mut().contract_info().size(key)
|
||||
}
|
||||
|
||||
fn set_storage(
|
||||
@@ -1234,8 +1259,7 @@ where
|
||||
take_old: bool,
|
||||
) -> Result<WriteOutcome, DispatchError> {
|
||||
let frame = self.top_frame_mut();
|
||||
Storage::<T>::write(
|
||||
&frame.contract_info.get(&frame.account_id).trie_id,
|
||||
frame.contract_info.get(&frame.account_id).write(
|
||||
key,
|
||||
value,
|
||||
Some(&mut frame.nested_storage),
|
||||
@@ -1250,8 +1274,7 @@ where
|
||||
take_old: bool,
|
||||
) -> Result<WriteOutcome, DispatchError> {
|
||||
let frame = self.top_frame_mut();
|
||||
Storage::<T>::write(
|
||||
&frame.contract_info.get(&frame.account_id).trie_id,
|
||||
frame.contract_info.get(&frame.account_id).write(
|
||||
key,
|
||||
value,
|
||||
Some(&mut frame.nested_storage),
|
||||
@@ -1438,7 +1461,6 @@ mod tests {
|
||||
use crate::{
|
||||
exec::ExportedFunction::*,
|
||||
gas::GasMeter,
|
||||
storage::Storage,
|
||||
tests::{
|
||||
test_utils::{get_balance, hash, place_contract, set_balance},
|
||||
ExtBuilder, RuntimeCall, RuntimeEvent as MetaEvent, Test, TestFilter, ALICE, BOB,
|
||||
@@ -1891,9 +1913,8 @@ mod tests {
|
||||
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
|
||||
let executable =
|
||||
MockExecutable::from_storage(input_data_ch, &schedule, &mut gas_meter).unwrap();
|
||||
set_balance(&ALICE, min_balance * 1000);
|
||||
let mut storage_meter =
|
||||
storage::meter::Meter::new(&ALICE, Some(min_balance * 100), min_balance).unwrap();
|
||||
set_balance(&ALICE, min_balance * 10_000);
|
||||
let mut storage_meter = storage::meter::Meter::new(&ALICE, None, min_balance).unwrap();
|
||||
|
||||
let result = MockStack::run_instantiate(
|
||||
ALICE,
|
||||
@@ -2231,7 +2252,7 @@ mod tests {
|
||||
// Check that the newly created account has the expected code hash and
|
||||
// there are instantiation event.
|
||||
assert_eq!(
|
||||
Storage::<Test>::code_hash(&instantiated_contract_address).unwrap(),
|
||||
ContractInfo::<Test>::load_code_hash(&instantiated_contract_address).unwrap(),
|
||||
dummy_ch
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2273,7 +2294,7 @@ mod tests {
|
||||
);
|
||||
|
||||
// Check that the account has not been created.
|
||||
assert!(Storage::<Test>::code_hash(&instantiated_contract_address).is_none());
|
||||
assert!(ContractInfo::<Test>::load_code_hash(&instantiated_contract_address).is_none());
|
||||
assert!(events().is_empty());
|
||||
});
|
||||
}
|
||||
@@ -2332,7 +2353,7 @@ mod tests {
|
||||
// Check that the newly created account has the expected code hash and
|
||||
// there are instantiation event.
|
||||
assert_eq!(
|
||||
Storage::<Test>::code_hash(&instantiated_contract_address).unwrap(),
|
||||
ContractInfo::<Test>::load_code_hash(&instantiated_contract_address).unwrap(),
|
||||
dummy_ch
|
||||
);
|
||||
assert_eq!(
|
||||
@@ -2374,7 +2395,7 @@ mod tests {
|
||||
set_balance(&ALICE, 1000);
|
||||
set_balance(&BOB, 100);
|
||||
place_contract(&BOB, instantiator_ch);
|
||||
let mut storage_meter = storage::meter::Meter::new(&ALICE, Some(100), 0).unwrap();
|
||||
let mut storage_meter = storage::meter::Meter::new(&ALICE, Some(200), 0).unwrap();
|
||||
|
||||
assert_matches!(
|
||||
MockStack::run_call(
|
||||
@@ -2409,8 +2430,8 @@ mod tests {
|
||||
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
|
||||
let executable =
|
||||
MockExecutable::from_storage(terminate_ch, &schedule, &mut gas_meter).unwrap();
|
||||
set_balance(&ALICE, 1000);
|
||||
let mut storage_meter = storage::meter::Meter::new(&ALICE, Some(100), 100).unwrap();
|
||||
set_balance(&ALICE, 10_000);
|
||||
let mut storage_meter = storage::meter::Meter::new(&ALICE, None, 100).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
MockStack::run_instantiate(
|
||||
@@ -2494,9 +2515,8 @@ mod tests {
|
||||
let min_balance = <Test as Config>::Currency::minimum_balance();
|
||||
let mut gas_meter = GasMeter::<Test>::new(GAS_LIMIT);
|
||||
let executable = MockExecutable::from_storage(code, &schedule, &mut gas_meter).unwrap();
|
||||
set_balance(&ALICE, min_balance * 1000);
|
||||
let mut storage_meter =
|
||||
storage::meter::Meter::new(&ALICE, Some(min_balance * 100), min_balance).unwrap();
|
||||
set_balance(&ALICE, min_balance * 10_000);
|
||||
let mut storage_meter = storage::meter::Meter::new(&ALICE, None, min_balance).unwrap();
|
||||
|
||||
let result = MockStack::run_instantiate(
|
||||
ALICE,
|
||||
@@ -2902,10 +2922,9 @@ mod tests {
|
||||
MockExecutable::from_storage(succ_fail_code, &schedule, &mut gas_meter).unwrap();
|
||||
let succ_succ_executable =
|
||||
MockExecutable::from_storage(succ_succ_code, &schedule, &mut gas_meter).unwrap();
|
||||
set_balance(&ALICE, min_balance * 1000);
|
||||
set_balance(&ALICE, min_balance * 10_000);
|
||||
let mut storage_meter =
|
||||
storage::meter::Meter::new(&ALICE, Some(min_balance * 500), min_balance * 100)
|
||||
.unwrap();
|
||||
storage::meter::Meter::new(&ALICE, None, min_balance * 100).unwrap();
|
||||
|
||||
MockStack::run_instantiate(
|
||||
ALICE,
|
||||
|
||||
@@ -85,6 +85,7 @@
|
||||
|
||||
#[macro_use]
|
||||
mod gas;
|
||||
mod address;
|
||||
mod benchmarking;
|
||||
mod exec;
|
||||
mod migration;
|
||||
@@ -101,11 +102,11 @@ mod tests;
|
||||
use crate::{
|
||||
exec::{AccountIdOf, ExecError, Executable, Stack as ExecStack},
|
||||
gas::GasMeter,
|
||||
storage::{meter::Meter as StorageMeter, ContractInfo, DeletedContract, Storage},
|
||||
storage::{meter::Meter as StorageMeter, ContractInfo, DeletedContract},
|
||||
wasm::{OwnerInfo, PrefabWasmModule, TryInstantiate},
|
||||
weights::WeightInfo,
|
||||
};
|
||||
use codec::{Codec, Decode, Encode, HasCompact};
|
||||
use codec::{Codec, Encode, HasCompact};
|
||||
use frame_support::{
|
||||
dispatch::{Dispatchable, GetDispatchInfo, Pays, PostDispatchInfo},
|
||||
ensure,
|
||||
@@ -124,10 +125,11 @@ use pallet_contracts_primitives::{
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use smallvec::Array;
|
||||
use sp_runtime::traits::{Convert, Hash, Saturating, StaticLookup, TrailingZeroInput};
|
||||
use sp_runtime::traits::{Convert, Hash, Saturating, StaticLookup};
|
||||
use sp_std::{fmt::Debug, marker::PhantomData, prelude::*};
|
||||
|
||||
pub use crate::{
|
||||
address::{AddressGenerator, DefaultAddressGenerator},
|
||||
exec::{Frame, VarSizedKey as StorageKey},
|
||||
migration::Migration,
|
||||
pallet::*,
|
||||
@@ -155,49 +157,6 @@ type DebugBufferVec<T> = BoundedVec<u8, <T as Config>::MaxDebugBufferLen>;
|
||||
/// that this value makes sense for a memory location or length.
|
||||
const SENTINEL: u32 = u32::MAX;
|
||||
|
||||
/// Provides the contract address generation method.
|
||||
///
|
||||
/// See [`DefaultAddressGenerator`] for the default implementation.
|
||||
pub trait AddressGenerator<T: Config> {
|
||||
/// Generate the address of a contract based on the given instantiate parameters.
|
||||
///
|
||||
/// # Note for implementors
|
||||
/// 1. Make sure that there are no collisions, different inputs never lead to the same output.
|
||||
/// 2. Make sure that the same inputs lead to the same output.
|
||||
/// 3. Changing the implementation through a runtime upgrade without a proper storage migration
|
||||
/// would lead to catastrophic misbehavior.
|
||||
fn generate_address(
|
||||
deploying_address: &T::AccountId,
|
||||
code_hash: &CodeHash<T>,
|
||||
input_data: &[u8],
|
||||
salt: &[u8],
|
||||
) -> T::AccountId;
|
||||
}
|
||||
|
||||
/// Default address generator.
|
||||
///
|
||||
/// This is the default address generator used by contract instantiation. Its result
|
||||
/// is only dependant on its inputs. It can therefore be used to reliably predict the
|
||||
/// address of a contract. This is akin to the formula of eth's CREATE2 opcode. There
|
||||
/// is no CREATE equivalent because CREATE2 is strictly more powerful.
|
||||
/// Formula:
|
||||
/// `hash("contract_addr_v1" ++ deploying_address ++ code_hash ++ input_data ++ salt)`
|
||||
pub struct DefaultAddressGenerator;
|
||||
|
||||
impl<T: Config> AddressGenerator<T> for DefaultAddressGenerator {
|
||||
fn generate_address(
|
||||
deploying_address: &T::AccountId,
|
||||
code_hash: &CodeHash<T>,
|
||||
input_data: &[u8],
|
||||
salt: &[u8],
|
||||
) -> T::AccountId {
|
||||
let entropy = (b"contract_addr_v1", deploying_address, code_hash, input_data, salt)
|
||||
.using_encoded(T::Hashing::hash);
|
||||
Decode::decode(&mut TrailingZeroInput::new(entropy.as_ref()))
|
||||
.expect("infinite length input; no invalid inputs for type; qed")
|
||||
}
|
||||
}
|
||||
|
||||
#[frame_support::pallet]
|
||||
pub mod pallet {
|
||||
use super::*;
|
||||
@@ -365,7 +324,7 @@ pub mod pallet {
|
||||
#[pallet::hooks]
|
||||
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
|
||||
fn on_idle(_block: T::BlockNumber, remaining_weight: Weight) -> Weight {
|
||||
Storage::<T>::process_deletion_queue_batch(remaining_weight)
|
||||
ContractInfo::<T>::process_deletion_queue_batch(remaining_weight)
|
||||
.saturating_add(T::WeightInfo::on_process_deletion_queue_batch())
|
||||
}
|
||||
|
||||
@@ -381,7 +340,7 @@ pub mod pallet {
|
||||
.max_block
|
||||
.saturating_sub(System::<T>::block_weight().total())
|
||||
.min(T::DeletionWeightLimit::get());
|
||||
Storage::<T>::process_deletion_queue_batch(weight_limit)
|
||||
ContractInfo::<T>::process_deletion_queue_batch(weight_limit)
|
||||
.saturating_add(T::WeightInfo::on_process_deletion_queue_batch())
|
||||
} else {
|
||||
T::WeightInfo::on_process_deletion_queue_batch()
|
||||
@@ -1129,8 +1088,7 @@ impl<T: Config> Pallet<T> {
|
||||
let contract_info =
|
||||
ContractInfoOf::<T>::get(&address).ok_or(ContractAccessError::DoesntExist)?;
|
||||
|
||||
let maybe_value = Storage::<T>::read(
|
||||
&contract_info.trie_id,
|
||||
let maybe_value = contract_info.read(
|
||||
&StorageKey::<T>::try_from(key).map_err(|_| ContractAccessError::KeyDecodingFailed)?,
|
||||
);
|
||||
Ok(maybe_value)
|
||||
@@ -1146,12 +1104,12 @@ impl<T: Config> Pallet<T> {
|
||||
input_data: &[u8],
|
||||
salt: &[u8],
|
||||
) -> T::AccountId {
|
||||
T::AddressGenerator::generate_address(deploying_address, code_hash, input_data, salt)
|
||||
T::AddressGenerator::contract_address(deploying_address, code_hash, input_data, salt)
|
||||
}
|
||||
|
||||
/// Returns the code hash of the contract specified by `account` ID.
|
||||
pub fn code_hash(account: &AccountIdOf<T>) -> Option<CodeHash<T>> {
|
||||
Storage::<T>::code_hash(account)
|
||||
ContractInfo::<T>::load_code_hash(account)
|
||||
}
|
||||
|
||||
/// Store code for benchmarks which does not check nor instrument the code.
|
||||
|
||||
@@ -22,13 +22,15 @@ pub mod meter;
|
||||
use crate::{
|
||||
exec::{AccountIdOf, StorageKey},
|
||||
weights::WeightInfo,
|
||||
BalanceOf, CodeHash, Config, ContractInfoOf, DeletionQueue, Error, TrieId, SENTINEL,
|
||||
AddressGenerator, BalanceOf, CodeHash, Config, ContractInfoOf, DeletionQueue, Error, Pallet,
|
||||
TrieId, SENTINEL,
|
||||
};
|
||||
use codec::{Decode, Encode, MaxEncodedLen};
|
||||
use frame_support::{
|
||||
dispatch::{DispatchError, DispatchResult},
|
||||
storage::child::{self, ChildInfo},
|
||||
weights::Weight,
|
||||
RuntimeDebugNoBound,
|
||||
};
|
||||
use scale_info::TypeInfo;
|
||||
use sp_io::KillStorageResult;
|
||||
@@ -36,7 +38,7 @@ use sp_runtime::{
|
||||
traits::{Hash, Saturating, Zero},
|
||||
RuntimeDebug,
|
||||
};
|
||||
use sp_std::{marker::PhantomData, prelude::*};
|
||||
use sp_std::{ops::Deref, prelude::*};
|
||||
|
||||
/// Information for managing an account and its sub trie abstraction.
|
||||
/// This is the required info to cache for an account.
|
||||
@@ -45,28 +47,68 @@ use sp_std::{marker::PhantomData, prelude::*};
|
||||
pub struct ContractInfo<T: Config> {
|
||||
/// Unique ID for the subtree encoded as a bytes vector.
|
||||
pub trie_id: TrieId,
|
||||
/// The account that holds this contracts storage deposit.
|
||||
///
|
||||
/// This is held in a separate account to prevent the contract from spending it.
|
||||
deposit_account: DepositAccount<T>,
|
||||
/// The code associated with a given account.
|
||||
pub code_hash: CodeHash<T>,
|
||||
/// How many bytes of storage are accumulated in this contract's child trie.
|
||||
pub storage_bytes: u32,
|
||||
storage_bytes: u32,
|
||||
/// How many items of storage are accumulated in this contract's child trie.
|
||||
pub storage_items: u32,
|
||||
storage_items: u32,
|
||||
/// This records to how much deposit the accumulated `storage_bytes` amount to.
|
||||
pub storage_byte_deposit: BalanceOf<T>,
|
||||
/// This records to how much deposit the accumulated `storage_items` amount to.
|
||||
pub storage_item_deposit: BalanceOf<T>,
|
||||
storage_item_deposit: BalanceOf<T>,
|
||||
/// This records how much deposit is put down in order to pay for the contract itself.
|
||||
///
|
||||
/// We need to store this information separately so it is not used when calculating any refunds
|
||||
/// since the base deposit can only ever be refunded on contract termination.
|
||||
pub storage_base_deposit: BalanceOf<T>,
|
||||
storage_base_deposit: BalanceOf<T>,
|
||||
}
|
||||
|
||||
impl<T: Config> ContractInfo<T> {
|
||||
/// Constructs a new contract info **without** writing it to storage.
|
||||
///
|
||||
/// This returns an `Err` if an contract with the supplied `account` already exists
|
||||
/// in storage.
|
||||
pub fn new(
|
||||
account: &AccountIdOf<T>,
|
||||
nonce: u64,
|
||||
code_hash: CodeHash<T>,
|
||||
) -> Result<Self, DispatchError> {
|
||||
if <ContractInfoOf<T>>::contains_key(account) {
|
||||
return Err(Error::<T>::DuplicateContract.into())
|
||||
}
|
||||
|
||||
let trie_id = {
|
||||
let buf = (account, nonce).using_encoded(T::Hashing::hash);
|
||||
buf.as_ref()
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("Runtime uses a reasonable hash size. Hence sizeof(T::Hash) <= 128; qed")
|
||||
};
|
||||
|
||||
let deposit_account = DepositAccount(T::AddressGenerator::deposit_address(account));
|
||||
|
||||
let contract = Self {
|
||||
trie_id,
|
||||
deposit_account,
|
||||
code_hash,
|
||||
storage_bytes: 0,
|
||||
storage_items: 0,
|
||||
storage_byte_deposit: Zero::zero(),
|
||||
storage_item_deposit: Zero::zero(),
|
||||
storage_base_deposit: Zero::zero(),
|
||||
};
|
||||
|
||||
Ok(contract)
|
||||
}
|
||||
|
||||
/// Associated child trie unique id is built from the hash part of the trie id.
|
||||
#[cfg(test)]
|
||||
pub fn child_trie_info(&self) -> ChildInfo {
|
||||
child_trie_info(&self.trie_id[..])
|
||||
ChildInfo::new_default(self.trie_id.as_ref())
|
||||
}
|
||||
|
||||
/// The deposit paying for the accumulated storage generated within the contract's child trie.
|
||||
@@ -76,79 +118,30 @@ impl<T: Config> ContractInfo<T> {
|
||||
|
||||
/// Same as [`Self::extra_deposit`] but including the base deposit.
|
||||
pub fn total_deposit(&self) -> BalanceOf<T> {
|
||||
self.extra_deposit().saturating_add(self.storage_base_deposit)
|
||||
}
|
||||
}
|
||||
|
||||
/// Associated child trie unique id is built from the hash part of the trie id.
|
||||
fn child_trie_info(trie_id: &[u8]) -> ChildInfo {
|
||||
ChildInfo::new_default(trie_id)
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct DeletedContract {
|
||||
pub(crate) trie_id: TrieId,
|
||||
}
|
||||
|
||||
/// Information about what happended to the pre-existing value when calling [`Storage::write`].
|
||||
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||
pub enum WriteOutcome {
|
||||
/// No value existed at the specified key.
|
||||
New,
|
||||
/// A value of the returned length was overwritten.
|
||||
Overwritten(u32),
|
||||
/// The returned value was taken out of storage before being overwritten.
|
||||
///
|
||||
/// This is only returned when specifically requested because it causes additional work
|
||||
/// depending on the size of the pre-existing value. When not requested [`Self::Overwritten`]
|
||||
/// is returned instead.
|
||||
Taken(Vec<u8>),
|
||||
}
|
||||
|
||||
impl WriteOutcome {
|
||||
/// Extracts the size of the overwritten value or `0` if there
|
||||
/// was no value in storage.
|
||||
pub fn old_len(&self) -> u32 {
|
||||
match self {
|
||||
Self::New => 0,
|
||||
Self::Overwritten(len) => *len,
|
||||
Self::Taken(value) => value.len() as u32,
|
||||
}
|
||||
self.extra_deposit()
|
||||
.saturating_add(self.storage_base_deposit)
|
||||
.saturating_sub(Pallet::<T>::min_balance())
|
||||
}
|
||||
|
||||
/// Extracts the size of the overwritten value or `SENTINEL` if there
|
||||
/// was no value in storage.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// We cannot use `0` as sentinel value because there could be a zero sized
|
||||
/// storage entry which is different from a non existing one.
|
||||
pub fn old_len_with_sentinel(&self) -> u32 {
|
||||
match self {
|
||||
Self::New => SENTINEL,
|
||||
Self::Overwritten(len) => *len,
|
||||
Self::Taken(value) => value.len() as u32,
|
||||
}
|
||||
/// Return the account that storage deposits should be deposited into.
|
||||
pub fn deposit_account(&self) -> &DepositAccount<T> {
|
||||
&self.deposit_account
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Storage<T>(PhantomData<T>);
|
||||
|
||||
impl<T: Config> Storage<T> {
|
||||
/// Reads a storage kv pair of a contract.
|
||||
///
|
||||
/// The read is performed from the `trie_id` only. The `address` is not necessary. If the
|
||||
/// contract doesn't store under the given `key` `None` is returned.
|
||||
pub fn read<K: StorageKey<T>>(trie_id: &TrieId, key: &K) -> Option<Vec<u8>> {
|
||||
child::get_raw(&child_trie_info(trie_id), key.hash().as_slice())
|
||||
pub fn read<K: StorageKey<T>>(&self, key: &K) -> Option<Vec<u8>> {
|
||||
child::get_raw(&self.child_trie_info(), key.hash().as_slice())
|
||||
}
|
||||
|
||||
/// Returns `Some(len)` (in bytes) if a storage item exists at `key`.
|
||||
///
|
||||
/// Returns `None` if the `key` wasn't previously set by `set_storage` or
|
||||
/// was deleted.
|
||||
pub fn size<K: StorageKey<T>>(trie_id: &TrieId, key: &K) -> Option<u32> {
|
||||
child::len(&child_trie_info(trie_id), key.hash().as_slice())
|
||||
pub fn size<K: StorageKey<T>>(&self, key: &K) -> Option<u32> {
|
||||
child::len(&self.child_trie_info(), key.hash().as_slice())
|
||||
}
|
||||
|
||||
/// Update a storage entry into a contract's kv storage.
|
||||
@@ -159,13 +152,13 @@ impl<T: Config> Storage<T> {
|
||||
/// This function also records how much storage was created or removed if a `storage_meter`
|
||||
/// is supplied. It should only be absent for testing or benchmarking code.
|
||||
pub fn write<K: StorageKey<T>>(
|
||||
trie_id: &TrieId,
|
||||
&self,
|
||||
key: &K,
|
||||
new_value: Option<Vec<u8>>,
|
||||
storage_meter: Option<&mut meter::NestedMeter<T>>,
|
||||
take: bool,
|
||||
) -> Result<WriteOutcome, DispatchError> {
|
||||
let child_trie_info = &child_trie_info(trie_id);
|
||||
let child_trie_info = &self.child_trie_info();
|
||||
let hashed_key = key.hash();
|
||||
let (old_len, old_value) = if take {
|
||||
let val = child::get_raw(child_trie_info, &hashed_key);
|
||||
@@ -208,37 +201,11 @@ impl<T: Config> Storage<T> {
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a new contract descriptor in the storage with the given code hash at the given
|
||||
/// address.
|
||||
///
|
||||
/// Returns `Err` if there is already a contract at the given address.
|
||||
pub fn new_contract(
|
||||
account: &AccountIdOf<T>,
|
||||
trie_id: TrieId,
|
||||
code_hash: CodeHash<T>,
|
||||
) -> Result<ContractInfo<T>, DispatchError> {
|
||||
if <ContractInfoOf<T>>::contains_key(account) {
|
||||
return Err(Error::<T>::DuplicateContract.into())
|
||||
}
|
||||
|
||||
let contract = ContractInfo::<T> {
|
||||
code_hash,
|
||||
trie_id,
|
||||
storage_bytes: 0,
|
||||
storage_items: 0,
|
||||
storage_byte_deposit: Zero::zero(),
|
||||
storage_item_deposit: Zero::zero(),
|
||||
storage_base_deposit: Zero::zero(),
|
||||
};
|
||||
|
||||
Ok(contract)
|
||||
}
|
||||
|
||||
/// Push a contract's trie to the deletion queue for lazy removal.
|
||||
///
|
||||
/// You must make sure that the contract is also removed when queuing the trie for deletion.
|
||||
pub fn queue_trie_for_deletion(contract: &ContractInfo<T>) -> DispatchResult {
|
||||
<DeletionQueue<T>>::try_append(DeletedContract { trie_id: contract.trie_id.clone() })
|
||||
pub fn queue_trie_for_deletion(&self) -> DispatchResult {
|
||||
<DeletionQueue<T>>::try_append(DeletedContract { trie_id: self.trie_id.clone() })
|
||||
.map_err(|_| <Error<T>>::DeletionQueueFull.into())
|
||||
}
|
||||
|
||||
@@ -289,7 +256,10 @@ impl<T: Config> Storage<T> {
|
||||
// Cannot panic due to loop condition
|
||||
let trie = &mut queue[0];
|
||||
#[allow(deprecated)]
|
||||
let outcome = child::kill_storage(&child_trie_info(&trie.trie_id), Some(remaining_key_budget));
|
||||
let outcome = child::kill_storage(
|
||||
&ChildInfo::new_default(&trie.trie_id),
|
||||
Some(remaining_key_budget),
|
||||
);
|
||||
let keys_removed = match outcome {
|
||||
// This happens when our budget wasn't large enough to remove all keys.
|
||||
KillStorageResult::SomeRemaining(c) => c,
|
||||
@@ -307,17 +277,8 @@ impl<T: Config> Storage<T> {
|
||||
weight_limit.saturating_sub(weight_per_key.saturating_mul(u64::from(remaining_key_budget)))
|
||||
}
|
||||
|
||||
/// Generates a unique trie id by returning `hash(account_id ++ nonce)`.
|
||||
pub fn generate_trie_id(account_id: &AccountIdOf<T>, nonce: u64) -> TrieId {
|
||||
let buf = (account_id, nonce).using_encoded(T::Hashing::hash);
|
||||
buf.as_ref()
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.expect("Runtime uses a reasonable hash size. Hence sizeof(T::Hash) <= 128; qed")
|
||||
}
|
||||
|
||||
/// Returns the code hash of the contract specified by `account` ID.
|
||||
pub fn code_hash(account: &AccountIdOf<T>) -> Option<CodeHash<T>> {
|
||||
pub fn load_code_hash(account: &AccountIdOf<T>) -> Option<CodeHash<T>> {
|
||||
<ContractInfoOf<T>>::get(account).map(|i| i.code_hash)
|
||||
}
|
||||
|
||||
@@ -332,3 +293,62 @@ impl<T: Config> Storage<T> {
|
||||
<DeletionQueue<T>>::put(bounded);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, TypeInfo, MaxEncodedLen)]
|
||||
pub struct DeletedContract {
|
||||
pub(crate) trie_id: TrieId,
|
||||
}
|
||||
|
||||
/// Information about what happended to the pre-existing value when calling [`ContractInfo::write`].
|
||||
#[cfg_attr(test, derive(Debug, PartialEq))]
|
||||
pub enum WriteOutcome {
|
||||
/// No value existed at the specified key.
|
||||
New,
|
||||
/// A value of the returned length was overwritten.
|
||||
Overwritten(u32),
|
||||
/// The returned value was taken out of storage before being overwritten.
|
||||
///
|
||||
/// This is only returned when specifically requested because it causes additional work
|
||||
/// depending on the size of the pre-existing value. When not requested [`Self::Overwritten`]
|
||||
/// is returned instead.
|
||||
Taken(Vec<u8>),
|
||||
}
|
||||
|
||||
impl WriteOutcome {
|
||||
/// Extracts the size of the overwritten value or `0` if there
|
||||
/// was no value in storage.
|
||||
pub fn old_len(&self) -> u32 {
|
||||
match self {
|
||||
Self::New => 0,
|
||||
Self::Overwritten(len) => *len,
|
||||
Self::Taken(value) => value.len() as u32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the size of the overwritten value or `SENTINEL` if there
|
||||
/// was no value in storage.
|
||||
///
|
||||
/// # Note
|
||||
///
|
||||
/// We cannot use `0` as sentinel value because there could be a zero sized
|
||||
/// storage entry which is different from a non existing one.
|
||||
pub fn old_len_with_sentinel(&self) -> u32 {
|
||||
match self {
|
||||
Self::New => SENTINEL,
|
||||
Self::Overwritten(len) => *len,
|
||||
Self::Taken(value) => value.len() as u32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebugNoBound, TypeInfo, MaxEncodedLen)]
|
||||
#[scale_info(skip_type_params(T))]
|
||||
pub struct DepositAccount<T: Config>(AccountIdOf<T>);
|
||||
|
||||
impl<T: Config> Deref for DepositAccount<T> {
|
||||
type Target = AccountIdOf<T>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,22 +17,19 @@
|
||||
|
||||
//! This module contains functions to meter the storage deposit.
|
||||
|
||||
use crate::{storage::ContractInfo, BalanceOf, Config, Error, Inspect, Pallet};
|
||||
use crate::{
|
||||
storage::{ContractInfo, DepositAccount},
|
||||
BalanceOf, Config, Error, Inspect, Pallet, System,
|
||||
};
|
||||
use codec::Encode;
|
||||
use frame_support::{
|
||||
dispatch::DispatchError,
|
||||
ensure,
|
||||
traits::{
|
||||
tokens::{BalanceStatus, WithdrawConsequence},
|
||||
Currency, ExistenceRequirement, Get, ReservableCurrency,
|
||||
},
|
||||
traits::{tokens::WithdrawConsequence, Currency, ExistenceRequirement, Get},
|
||||
DefaultNoBound, RuntimeDebugNoBound,
|
||||
};
|
||||
use pallet_contracts_primitives::StorageDeposit as Deposit;
|
||||
use sp_runtime::{
|
||||
traits::{Saturating, Zero},
|
||||
FixedPointNumber, FixedU128,
|
||||
};
|
||||
use sp_runtime::{traits::Saturating, FixedPointNumber, FixedU128};
|
||||
use sp_std::{marker::PhantomData, vec::Vec};
|
||||
|
||||
/// Deposit that uses the native currency's balance type.
|
||||
@@ -72,14 +69,14 @@ pub trait Ext<T: Config> {
|
||||
/// This is called to inform the implementer that some balance should be charged due to
|
||||
/// some interaction of the `origin` with a `contract`.
|
||||
///
|
||||
/// The balance transfer can either flow from `origin` to `contract` or the other way
|
||||
/// The balance transfer can either flow from `origin` to `deposit_account` or the other way
|
||||
/// around depending on whether `amount` constitutes a `Charge` or a `Refund`.
|
||||
/// It is guaranteed that that this succeeds because no more balance than returned by
|
||||
/// It is guaranteed that this succeeds because no more balance than returned by
|
||||
/// `check_limit` is ever charged. This is why this function is infallible.
|
||||
/// `terminated` designates whether the `contract` was terminated.
|
||||
fn charge(
|
||||
origin: &T::AccountId,
|
||||
contract: &T::AccountId,
|
||||
deposit_account: &DepositAccount<T>,
|
||||
amount: &DepositOf<T>,
|
||||
terminated: bool,
|
||||
);
|
||||
@@ -216,7 +213,7 @@ impl Diff {
|
||||
/// essentially makes the order of storage changes irrelevant with regard to the deposit system.
|
||||
#[derive(RuntimeDebugNoBound, Clone)]
|
||||
struct Charge<T: Config> {
|
||||
contract: T::AccountId,
|
||||
deposit_account: DepositAccount<T>,
|
||||
amount: DepositOf<T>,
|
||||
terminated: bool,
|
||||
}
|
||||
@@ -270,8 +267,8 @@ where
|
||||
/// Absorb a child that was spawned to handle a sub call.
|
||||
///
|
||||
/// This should be called whenever a sub call comes to its end and it is **not** reverted.
|
||||
/// This does the actual balance transfer from/to `origin` and `contract` based on the overall
|
||||
/// storage consumption of the call. It also updates the supplied contract info.
|
||||
/// This does the actual balance transfer from/to `origin` and `deposit_account` based on the
|
||||
/// overall storage consumption of the call. It also updates the supplied contract info.
|
||||
///
|
||||
/// In case a contract reverted the child meter should just be dropped in order to revert
|
||||
/// any changes it recorded.
|
||||
@@ -280,12 +277,12 @@ where
|
||||
///
|
||||
/// - `absorbed`: The child storage meter that should be absorbed.
|
||||
/// - `origin`: The origin that spawned the original root meter.
|
||||
/// - `contract`: The contract that this sub call belongs to.
|
||||
/// - `deposit_account`: The contract's deposit account that this sub call belongs to.
|
||||
/// - `info`: The info of the contract in question. `None` if the contract was terminated.
|
||||
pub fn absorb(
|
||||
&mut self,
|
||||
absorbed: RawMeter<T, E, Nested>,
|
||||
contract: &T::AccountId,
|
||||
deposit_account: DepositAccount<T>,
|
||||
info: Option<&mut ContractInfo<T>>,
|
||||
) {
|
||||
let own_deposit = absorbed.own_contribution.update_contract(info);
|
||||
@@ -296,7 +293,7 @@ where
|
||||
if !own_deposit.is_zero() {
|
||||
self.charges.extend_from_slice(&absorbed.charges);
|
||||
self.charges.push(Charge {
|
||||
contract: contract.clone(),
|
||||
deposit_account,
|
||||
amount: own_deposit,
|
||||
terminated: absorbed.is_terminated(),
|
||||
});
|
||||
@@ -345,10 +342,10 @@ where
|
||||
/// execution did finish.
|
||||
pub fn into_deposit(self, origin: &T::AccountId) -> DepositOf<T> {
|
||||
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Refund(_))) {
|
||||
E::charge(origin, &charge.contract, &charge.amount, charge.terminated);
|
||||
E::charge(origin, &charge.deposit_account, &charge.amount, charge.terminated);
|
||||
}
|
||||
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Charge(_))) {
|
||||
E::charge(origin, &charge.contract, &charge.amount, charge.terminated);
|
||||
E::charge(origin, &charge.deposit_account, &charge.amount, charge.terminated);
|
||||
}
|
||||
self.total_deposit
|
||||
}
|
||||
@@ -360,9 +357,8 @@ where
|
||||
T: Config,
|
||||
E: Ext<T>,
|
||||
{
|
||||
/// Try to charge the `diff` from the meter. Fails if this would exceed the original limit.
|
||||
/// Charge `diff` from the meter.
|
||||
pub fn charge(&mut self, diff: &Diff) {
|
||||
debug_assert!(self.is_alive());
|
||||
match &mut self.own_contribution {
|
||||
Contribution::Alive(own) => *own = own.saturating_add(diff),
|
||||
_ => panic!("Charge is never called after termination; qed"),
|
||||
@@ -379,13 +375,16 @@ where
|
||||
info: &mut ContractInfo<T>,
|
||||
) -> Result<DepositOf<T>, DispatchError> {
|
||||
debug_assert!(self.is_alive());
|
||||
|
||||
let ed = Pallet::<T>::min_balance();
|
||||
let mut deposit =
|
||||
Diff { bytes_added: info.encoded_size() as u32, items_added: 1, ..Default::default() }
|
||||
.update_contract::<T>(None);
|
||||
|
||||
// Instantiate needs to transfer the minimum balance at least in order to pull the
|
||||
// contract's account into existence.
|
||||
deposit = deposit.max(Deposit::Charge(Pallet::<T>::min_balance()));
|
||||
// Instantiate needs to transfer at least the minimum balance in order to pull the
|
||||
// deposit account into existence.
|
||||
// We also add another `ed` here which goes to the contract's own account into existence.
|
||||
deposit = deposit.max(Deposit::Charge(ed)).saturating_add(&Deposit::Charge(ed));
|
||||
if deposit.charge_or_zero() > self.limit {
|
||||
return Err(<Error<T>>::StorageDepositLimitExhausted.into())
|
||||
}
|
||||
@@ -394,11 +393,22 @@ where
|
||||
// contract execution does conclude and hence would lead to a double charge.
|
||||
self.total_deposit = deposit.clone();
|
||||
info.storage_base_deposit = deposit.charge_or_zero();
|
||||
if !deposit.is_zero() {
|
||||
// We need to charge immediately so that the account is created before the `value`
|
||||
// is transferred from the caller to the contract.
|
||||
E::charge(origin, contract, &deposit, false);
|
||||
}
|
||||
|
||||
// Usually, deposit charges are deferred to be able to coalesce them with refunds.
|
||||
// However, we need to charge immediately so that the account is created before
|
||||
// charges possibly below the ed are collected and fail.
|
||||
E::charge(
|
||||
origin,
|
||||
info.deposit_account(),
|
||||
&deposit.saturating_sub(&Deposit::Charge(ed)),
|
||||
false,
|
||||
);
|
||||
System::<T>::inc_consumers(info.deposit_account())?;
|
||||
|
||||
// We also need to make sure that the contract's account itself exists.
|
||||
T::Currency::transfer(origin, contract, ed, ExistenceRequirement::KeepAlive)?;
|
||||
System::<T>::inc_consumers(contract)?;
|
||||
|
||||
Ok(deposit)
|
||||
}
|
||||
|
||||
@@ -443,7 +453,12 @@ impl<T: Config> Ext<T> for ReservingExt {
|
||||
limit: Option<BalanceOf<T>>,
|
||||
min_leftover: BalanceOf<T>,
|
||||
) -> Result<BalanceOf<T>, DispatchError> {
|
||||
let max = T::Currency::reducible_balance(origin, true).saturating_sub(min_leftover);
|
||||
// We are sending the `min_leftover` and the `min_balance` from the origin
|
||||
// account as part of a contract call. Hence origin needs to have those left over
|
||||
// as free balance after accounting for all deposits.
|
||||
let max = T::Currency::reducible_balance(origin, true)
|
||||
.saturating_sub(min_leftover)
|
||||
.saturating_sub(Pallet::<T>::min_balance());
|
||||
let limit = limit.unwrap_or(max);
|
||||
ensure!(
|
||||
limit <= max &&
|
||||
@@ -455,67 +470,67 @@ impl<T: Config> Ext<T> for ReservingExt {
|
||||
|
||||
fn charge(
|
||||
origin: &T::AccountId,
|
||||
contract: &T::AccountId,
|
||||
deposit_account: &DepositAccount<T>,
|
||||
amount: &DepositOf<T>,
|
||||
terminated: bool,
|
||||
) {
|
||||
// There is nothing we can do when this fails as this constitutes a bug in the runtime:
|
||||
// Either the runtime does not hold up the invariant of never deleting a contract's account
|
||||
// or it does not honor reserved balances. We need to settle for emitting an error log
|
||||
// in this case.
|
||||
// There is nothing we can do when this fails as this constitutes a bug in the runtime.
|
||||
// We need to settle for emitting an error log in this case.
|
||||
//
|
||||
// # Note
|
||||
//
|
||||
// This is infallible because it is called in a part of the execution where we cannot
|
||||
// simply roll back. It might make sense to do some refactoring to move the deposit
|
||||
// collection to the fallible part of execution.
|
||||
match amount {
|
||||
Deposit::Charge(amount) => {
|
||||
// This will never fail because a contract's account is required to exist
|
||||
// at all times. The pallet enforces this invariant by depositing at least the
|
||||
// existential deposit when instantiating and never refunds it unless the contract
|
||||
// is removed. This means the receiver always exists except when instantiating a
|
||||
// contract. In this case we made sure that at least the existential deposit is
|
||||
// sent. The sender always has enough balance because we checked that it had enough
|
||||
// balance when instantiating the storage meter.
|
||||
// This will never fail because a deposit account is required to exist
|
||||
// at all times. The pallet enforces this invariant by holding a consumer reference
|
||||
// on the deposit account as long as the contract exists.
|
||||
//
|
||||
// The sender always has enough balance because we checked that it had enough
|
||||
// balance when instantiating the storage meter. There is no way for the sender
|
||||
// which is a plain account to send away this balance in the meantime.
|
||||
let result = T::Currency::transfer(
|
||||
origin,
|
||||
contract,
|
||||
deposit_account,
|
||||
*amount,
|
||||
ExistenceRequirement::KeepAlive,
|
||||
)
|
||||
.and_then(|_| T::Currency::reserve(contract, *amount));
|
||||
);
|
||||
if let Err(err) = result {
|
||||
log::error!(
|
||||
target: "runtime::contracts",
|
||||
"Failed to transfer storage deposit {:?} from origin {:?} to contract {:?}: {:?}",
|
||||
amount, origin, contract, err,
|
||||
"Failed to transfer storage deposit {:?} from origin {:?} to deposit account {:?}: {:?}",
|
||||
amount, origin, deposit_account, err,
|
||||
);
|
||||
if cfg!(debug_assertions) {
|
||||
panic!("Unable to collect storage deposit. This is a bug.");
|
||||
}
|
||||
}
|
||||
},
|
||||
// For `Refund(_)` no error happen because the initial value transfer from the
|
||||
// origin to the contract has a keep alive existence requirement and when reserving we
|
||||
// make sure to leave at least the ed in the free balance. Therefore the receiver always
|
||||
// exists because there is no way for it to be removed in between. The sender always has
|
||||
// enough reserved balance because we track it in the `ContractInfo` and never send more
|
||||
// back than we have.
|
||||
// The receiver always exists because the initial value transfer from the
|
||||
// origin to the contract has a keep alive existence requirement. When taking a deposit
|
||||
// we make sure to leave at least the ed in the free balance.
|
||||
//
|
||||
// The sender always has enough balance because we track it in the `ContractInfo` and
|
||||
// never send more back than we have. Noone has access to the deposit account. Hence no
|
||||
// other interaction with this account takes place.
|
||||
Deposit::Refund(amount) => {
|
||||
let amount = if terminated {
|
||||
*amount
|
||||
} else {
|
||||
// This is necessary when the `storage_deposit` tracked inside the account
|
||||
// info is out of sync with the actual balance. That can only happen due to
|
||||
// slashing. We make sure to never dust the contract's account through a
|
||||
// refund because we consider this unexpected behaviour.
|
||||
*amount.min(
|
||||
&T::Currency::reserved_balance(contract)
|
||||
.saturating_sub(Pallet::<T>::min_balance()),
|
||||
)
|
||||
};
|
||||
let result =
|
||||
T::Currency::repatriate_reserved(contract, origin, amount, BalanceStatus::Free);
|
||||
if matches!(result, Ok(val) if !val.is_zero()) || matches!(result, Err(_)) {
|
||||
if terminated {
|
||||
System::<T>::dec_consumers(&deposit_account);
|
||||
}
|
||||
let result = T::Currency::transfer(
|
||||
deposit_account,
|
||||
origin,
|
||||
*amount,
|
||||
// We can safely use `AllowDeath` because our own consumer prevents an removal.
|
||||
ExistenceRequirement::AllowDeath,
|
||||
);
|
||||
if matches!(result, Err(_)) {
|
||||
log::error!(
|
||||
target: "runtime::contracts",
|
||||
"Failed to repatriate storage deposit {:?} from contract {:?} to origin {:?}: {:?}",
|
||||
amount, contract, origin, result,
|
||||
"Failed to refund storage deposit {:?} from deposit account {:?} to origin {:?}: {:?}",
|
||||
amount, deposit_account, origin, result,
|
||||
);
|
||||
if cfg!(debug_assertions) {
|
||||
panic!("Unable to refund storage deposit. This is a bug.");
|
||||
@@ -558,7 +573,7 @@ mod tests {
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
struct Charge {
|
||||
origin: AccountIdOf<Test>,
|
||||
contract: AccountIdOf<Test>,
|
||||
contract: DepositAccount<Test>,
|
||||
amount: DepositOf<Test>,
|
||||
terminated: bool,
|
||||
}
|
||||
@@ -592,7 +607,7 @@ mod tests {
|
||||
|
||||
fn charge(
|
||||
origin: &AccountIdOf<Test>,
|
||||
contract: &AccountIdOf<Test>,
|
||||
contract: &DepositAccount<Test>,
|
||||
amount: &DepositOf<Test>,
|
||||
terminated: bool,
|
||||
) {
|
||||
@@ -620,12 +635,10 @@ mod tests {
|
||||
}
|
||||
|
||||
fn new_info(info: StorageInfo) -> ContractInfo<Test> {
|
||||
use crate::storage::Storage;
|
||||
use sp_runtime::traits::Hash;
|
||||
|
||||
ContractInfo::<Test> {
|
||||
trie_id: <Storage<Test>>::generate_trie_id(&ALICE, 42),
|
||||
code_hash: <Test as frame_system::Config>::Hashing::hash(b"42"),
|
||||
trie_id: Default::default(),
|
||||
deposit_account: DepositAccount([0u8; 32].into()),
|
||||
code_hash: Default::default(),
|
||||
storage_bytes: info.bytes,
|
||||
storage_items: info.items,
|
||||
storage_byte_deposit: info.bytes_deposit,
|
||||
@@ -659,7 +672,7 @@ mod tests {
|
||||
// an empty charge does not create a `Charge` entry
|
||||
let mut nested0 = meter.nested();
|
||||
nested0.charge(&Default::default());
|
||||
meter.absorb(nested0, &BOB, None);
|
||||
meter.absorb(nested0, DepositAccount(BOB), None);
|
||||
|
||||
assert_eq!(
|
||||
TestExtTestValue::get(),
|
||||
@@ -692,16 +705,16 @@ mod tests {
|
||||
new_info(StorageInfo { bytes: 100, items: 10, bytes_deposit: 100, items_deposit: 20 });
|
||||
let mut nested1 = nested0.nested();
|
||||
nested1.charge(&Diff { items_removed: 5, ..Default::default() });
|
||||
nested0.absorb(nested1, &CHARLIE, Some(&mut nested1_info));
|
||||
nested0.absorb(nested1, DepositAccount(CHARLIE), Some(&mut nested1_info));
|
||||
|
||||
let mut nested2_info =
|
||||
new_info(StorageInfo { bytes: 100, items: 7, bytes_deposit: 100, items_deposit: 20 });
|
||||
let mut nested2 = nested0.nested();
|
||||
nested2.charge(&Diff { items_removed: 7, ..Default::default() });
|
||||
nested0.absorb(nested2, &CHARLIE, Some(&mut nested2_info));
|
||||
nested0.absorb(nested2, DepositAccount(CHARLIE), Some(&mut nested2_info));
|
||||
|
||||
nested0.enforce_limit(Some(&mut nested0_info)).unwrap();
|
||||
meter.absorb(nested0, &BOB, Some(&mut nested0_info));
|
||||
meter.absorb(nested0, DepositAccount(BOB), Some(&mut nested0_info));
|
||||
|
||||
meter.into_deposit(&ALICE);
|
||||
|
||||
@@ -716,19 +729,19 @@ mod tests {
|
||||
charges: vec![
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: CHARLIE,
|
||||
contract: DepositAccount(CHARLIE),
|
||||
amount: Deposit::Refund(10),
|
||||
terminated: false
|
||||
},
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: CHARLIE,
|
||||
contract: DepositAccount(CHARLIE),
|
||||
amount: Deposit::Refund(20),
|
||||
terminated: false
|
||||
},
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: BOB,
|
||||
contract: DepositAccount(BOB),
|
||||
amount: Deposit::Charge(2),
|
||||
terminated: false
|
||||
}
|
||||
@@ -760,9 +773,9 @@ mod tests {
|
||||
nested1.charge(&Diff { bytes_added: 20, ..Default::default() });
|
||||
nested1.terminate(&nested1_info);
|
||||
nested0.enforce_limit(Some(&mut nested1_info)).unwrap();
|
||||
nested0.absorb(nested1, &CHARLIE, None);
|
||||
nested0.absorb(nested1, DepositAccount(CHARLIE), None);
|
||||
|
||||
meter.absorb(nested0, &BOB, None);
|
||||
meter.absorb(nested0, DepositAccount(BOB), None);
|
||||
meter.into_deposit(&ALICE);
|
||||
|
||||
assert_eq!(
|
||||
@@ -772,13 +785,13 @@ mod tests {
|
||||
charges: vec![
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: CHARLIE,
|
||||
amount: Deposit::Refund(120),
|
||||
contract: DepositAccount(CHARLIE),
|
||||
amount: Deposit::Refund(119),
|
||||
terminated: true
|
||||
},
|
||||
Charge {
|
||||
origin: ALICE,
|
||||
contract: BOB,
|
||||
contract: DepositAccount(BOB),
|
||||
amount: Deposit::Charge(12),
|
||||
terminated: false
|
||||
}
|
||||
|
||||
@@ -22,12 +22,11 @@ use crate::{
|
||||
Result as ExtensionResult, RetVal, ReturnFlags, SysConfig,
|
||||
},
|
||||
exec::{FixSizedKey, Frame},
|
||||
storage::Storage,
|
||||
tests::test_utils::{get_contract, get_contract_checked},
|
||||
wasm::{Determinism, PrefabWasmModule, ReturnCode as RuntimeReturnCode},
|
||||
weights::WeightInfo,
|
||||
BalanceOf, Code, CodeStorage, Config, ContractInfoOf, DefaultAddressGenerator, DeletionQueue,
|
||||
Error, Pallet, Schedule,
|
||||
BalanceOf, Code, CodeStorage, Config, ContractInfo, ContractInfoOf, DefaultAddressGenerator,
|
||||
DeletionQueue, Error, Pallet, Schedule,
|
||||
};
|
||||
use assert_matches::assert_matches;
|
||||
use codec::Encode;
|
||||
@@ -37,8 +36,8 @@ use frame_support::{
|
||||
parameter_types,
|
||||
storage::child,
|
||||
traits::{
|
||||
BalanceStatus, ConstU32, ConstU64, Contains, Currency, Get, LockableCurrency, OnIdle,
|
||||
OnInitialize, ReservableCurrency, WithdrawReasons,
|
||||
ConstU32, ConstU64, Contains, Currency, ExistenceRequirement, Get, LockableCurrency,
|
||||
OnIdle, OnInitialize, ReservableCurrency, WithdrawReasons,
|
||||
},
|
||||
weights::{constants::WEIGHT_REF_TIME_PER_SECOND, Weight},
|
||||
};
|
||||
@@ -51,7 +50,7 @@ use sp_runtime::{
|
||||
traits::{BlakeTwo256, Convert, Hash, IdentityLookup},
|
||||
AccountId32,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use std::{ops::Deref, sync::Arc};
|
||||
|
||||
use crate as pallet_contracts;
|
||||
|
||||
@@ -76,9 +75,7 @@ frame_support::construct_runtime!(
|
||||
#[macro_use]
|
||||
pub mod test_utils {
|
||||
use super::{Balances, Hash, SysConfig, Test};
|
||||
use crate::{
|
||||
exec::AccountIdOf, storage::Storage, CodeHash, Config, ContractInfo, ContractInfoOf, Nonce,
|
||||
};
|
||||
use crate::{exec::AccountIdOf, CodeHash, Config, ContractInfo, ContractInfoOf, Nonce};
|
||||
use codec::Encode;
|
||||
use frame_support::traits::Currency;
|
||||
|
||||
@@ -87,9 +84,8 @@ pub mod test_utils {
|
||||
*counter += 1;
|
||||
*counter
|
||||
});
|
||||
let trie_id = Storage::<Test>::generate_trie_id(address, nonce);
|
||||
set_balance(address, <Test as Config>::Currency::minimum_balance() * 10);
|
||||
let contract = Storage::<Test>::new_contract(&address, trie_id, code_hash).unwrap();
|
||||
let contract = <ContractInfo<Test>>::new(&address, nonce, code_hash).unwrap();
|
||||
<ContractInfoOf<Test>>::insert(address, contract);
|
||||
}
|
||||
pub fn set_balance(who: &AccountIdOf<Test>, amount: u64) {
|
||||
@@ -518,7 +514,7 @@ fn calling_plain_account_fails() {
|
||||
fn instantiate_and_call_and_deposit_event() {
|
||||
let (wasm, code_hash) = compile_module::<Test>("event_and_return_on_deploy").unwrap();
|
||||
|
||||
ExtBuilder::default().existential_deposit(500).build().execute_with(|| {
|
||||
ExtBuilder::default().existential_deposit(1).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
let min_balance = <Test as Config>::Currency::minimum_balance();
|
||||
let value = 100;
|
||||
@@ -551,9 +547,36 @@ fn instantiate_and_call_and_deposit_event() {
|
||||
.account_id;
|
||||
assert!(ContractInfoOf::<Test>::contains_key(&addr));
|
||||
|
||||
let contract = get_contract(&addr);
|
||||
let deposit_account = contract.deposit_account().deref();
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
account: deposit_account.clone(),
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Endowed {
|
||||
account: deposit_account.clone(),
|
||||
free_balance: 131,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: ALICE,
|
||||
to: deposit_account.clone(),
|
||||
amount: 131,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
@@ -578,14 +601,6 @@ fn instantiate_and_call_and_deposit_event() {
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
|
||||
who: addr.clone(),
|
||||
amount: min_balance,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
@@ -823,7 +838,7 @@ fn deploy_and_call_other_contract() {
|
||||
let (caller_wasm, _caller_code_hash) = compile_module::<Test>("caller_contract").unwrap();
|
||||
let (callee_wasm, callee_code_hash) = compile_module::<Test>("return_with_data").unwrap();
|
||||
|
||||
ExtBuilder::default().existential_deposit(500).build().execute_with(|| {
|
||||
ExtBuilder::default().existential_deposit(1).build().execute_with(|| {
|
||||
let min_balance = <Test as Config>::Currency::minimum_balance();
|
||||
|
||||
// Create
|
||||
@@ -841,18 +856,7 @@ fn deploy_and_call_other_contract() {
|
||||
.result
|
||||
.unwrap()
|
||||
.account_id;
|
||||
Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
100_000,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
Code::Upload(callee_wasm),
|
||||
0u32.to_le_bytes().encode(),
|
||||
vec![42],
|
||||
false,
|
||||
)
|
||||
.result
|
||||
.unwrap();
|
||||
Contracts::bare_upload_code(ALICE, callee_wasm, None, Determinism::Deterministic).unwrap();
|
||||
|
||||
let callee_addr = Contracts::contract_address(
|
||||
&caller_addr,
|
||||
@@ -875,9 +879,36 @@ fn deploy_and_call_other_contract() {
|
||||
callee_code_hash.as_ref().to_vec(),
|
||||
));
|
||||
|
||||
let callee = get_contract(&callee_addr);
|
||||
let deposit_account = callee.deposit_account().deref();
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
account: deposit_account.clone(),
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Endowed {
|
||||
account: deposit_account.clone(),
|
||||
free_balance: 131,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: ALICE,
|
||||
to: deposit_account.clone(),
|
||||
amount: 131,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
@@ -902,20 +933,12 @@ fn deploy_and_call_other_contract() {
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
|
||||
who: callee_addr.clone(),
|
||||
amount: min_balance,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: caller_addr.clone(),
|
||||
to: callee_addr.clone(),
|
||||
amount: 32768, // hard coded in wasm
|
||||
amount: 32768 // hardcoded in wasm
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
@@ -998,6 +1021,46 @@ fn delegate_call() {
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transfer_allow_death_cannot_kill_account() {
|
||||
let (wasm, _code_hash) = compile_module::<Test>("dummy").unwrap();
|
||||
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
|
||||
// Instantiate the BOB contract.
|
||||
let addr = Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
1_000,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
Code::Upload(wasm),
|
||||
vec![],
|
||||
vec![],
|
||||
false,
|
||||
)
|
||||
.result
|
||||
.unwrap()
|
||||
.account_id;
|
||||
|
||||
// Check that the BOB contract has been instantiated.
|
||||
get_contract(&addr);
|
||||
|
||||
let total_balance = <Test as Config>::Currency::total_balance(&addr);
|
||||
|
||||
assert_err!(
|
||||
<<Test as Config>::Currency as Currency<AccountId32>>::transfer(
|
||||
&addr,
|
||||
&ALICE,
|
||||
total_balance,
|
||||
ExistenceRequirement::AllowDeath,
|
||||
),
|
||||
pallet_balances::Error::<Test>::KeepAlive,
|
||||
);
|
||||
|
||||
assert_eq!(<Test as Config>::Currency::total_balance(&addr), total_balance);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_self_destruct_through_draning() {
|
||||
let (wasm, _code_hash) = compile_module::<Test>("drain").unwrap();
|
||||
@@ -1023,7 +1086,7 @@ fn cannot_self_destruct_through_draning() {
|
||||
get_contract(&addr);
|
||||
|
||||
// Call BOB which makes it send all funds to the zero address
|
||||
// The contract code asserts that the transfer was successful
|
||||
// The contract code asserts that the transfer fails with the correct error code
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr.clone(),
|
||||
@@ -1036,7 +1099,7 @@ fn cannot_self_destruct_through_draning() {
|
||||
// Make sure the account wasn't remove by sending all free balance away.
|
||||
assert_eq!(
|
||||
<Test as Config>::Currency::total_balance(&addr),
|
||||
<Test as Config>::Currency::minimum_balance(),
|
||||
1_000 + <Test as Config>::Currency::minimum_balance(),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -1093,96 +1156,10 @@ fn cannot_self_destruct_through_storage_refund_after_price_change() {
|
||||
|
||||
// Make sure the account wasn't removed by the refund
|
||||
assert_eq!(
|
||||
<Test as Config>::Currency::total_balance(&addr),
|
||||
<Test as Config>::Currency::total_balance(get_contract(&addr).deposit_account()),
|
||||
get_contract(&addr).total_deposit(),
|
||||
);
|
||||
assert_eq!(get_contract(&addr).extra_deposit(), 2,);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cannot_self_destruct_by_refund_after_slash() {
|
||||
let (wasm, _code_hash) = compile_module::<Test>("store").unwrap();
|
||||
ExtBuilder::default().existential_deposit(500).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
let min_balance = <Test as Config>::Currency::minimum_balance();
|
||||
|
||||
let addr = Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
Code::Upload(wasm),
|
||||
vec![],
|
||||
vec![],
|
||||
false,
|
||||
)
|
||||
.result
|
||||
.unwrap()
|
||||
.account_id;
|
||||
|
||||
// create 100 more reserved balance
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
98u32.encode(),
|
||||
));
|
||||
|
||||
// Drop previous events
|
||||
initialize_block(2);
|
||||
|
||||
// slash parts of the 100 so that the next refund ould remove the account
|
||||
// because it the value it stored for `storage_deposit` becomes out of sync
|
||||
let _ = <Test as Config>::Currency::slash(&addr, 90);
|
||||
assert_eq!(<Test as Config>::Currency::total_balance(&addr), min_balance + 10);
|
||||
|
||||
// trigger a refund of 50 which would bring the contract below min when actually refunded
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
48u32.encode(),
|
||||
));
|
||||
|
||||
// Make sure the account kept the minimum balance and was not destroyed
|
||||
assert_eq!(<Test as Config>::Currency::total_balance(&addr), min_balance);
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Slashed {
|
||||
who: addr.clone(),
|
||||
amount: 90,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Contracts(crate::Event::Called {
|
||||
caller: ALICE,
|
||||
contract: addr.clone(),
|
||||
}),
|
||||
topics: vec![hash(&ALICE), hash(&addr)],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::ReserveRepatriated {
|
||||
from: addr.clone(),
|
||||
to: ALICE,
|
||||
amount: 10,
|
||||
destination_status: BalanceStatus::Free,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
]
|
||||
);
|
||||
assert_eq!(get_contract(&addr).extra_deposit(), 2);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1252,7 +1229,7 @@ fn self_destruct_works() {
|
||||
.account_id;
|
||||
|
||||
// Check that the BOB contract has been instantiated.
|
||||
get_contract(&addr);
|
||||
let contract = get_contract(&addr);
|
||||
|
||||
// Drop all previous events
|
||||
initialize_block(2);
|
||||
@@ -1271,17 +1248,25 @@ fn self_destruct_works() {
|
||||
assert_eq!(Balances::total_balance(&addr), 0);
|
||||
|
||||
// check that the beneficiary (django) got remaining balance
|
||||
assert_eq!(Balances::free_balance(DJANGO), 1_000_000 + 100_000);
|
||||
let ed = <Test as Config>::Currency::minimum_balance();
|
||||
assert_eq!(Balances::free_balance(DJANGO), 1_000_000 + 100_000 + ed);
|
||||
|
||||
pretty_assertions::assert_eq!(
|
||||
System::events(),
|
||||
vec![
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::KilledAccount {
|
||||
account: addr.clone()
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: addr.clone(),
|
||||
to: DJANGO,
|
||||
amount: 100_000,
|
||||
amount: 100_000 + ed,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
@@ -1304,17 +1289,16 @@ fn self_destruct_works() {
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::KilledAccount {
|
||||
account: addr.clone()
|
||||
account: contract.deposit_account().deref().clone(),
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::ReserveRepatriated {
|
||||
from: addr.clone(),
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: contract.deposit_account().deref().clone(),
|
||||
to: ALICE,
|
||||
amount: 1_000,
|
||||
destination_status: BalanceStatus::Free,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
@@ -2055,7 +2039,7 @@ fn lazy_removal_on_full_queue_works_on_initialize() {
|
||||
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
|
||||
// Fill the deletion queue with dummy values, so that on_initialize attempts
|
||||
// to clear the queue
|
||||
Storage::<Test>::fill_queue_with_dummies();
|
||||
ContractInfo::<Test>::fill_queue_with_dummies();
|
||||
|
||||
let queue_len_initial = <DeletionQueue<Test>>::decode_len().unwrap_or(0);
|
||||
|
||||
@@ -2132,7 +2116,7 @@ fn lazy_removal_partial_remove_works() {
|
||||
// We create a contract with some extra keys above the weight limit
|
||||
let extra_keys = 7u32;
|
||||
let weight_limit = Weight::from_ref_time(5_000_000_000);
|
||||
let (_, max_keys) = Storage::<Test>::deletion_budget(1, weight_limit);
|
||||
let (_, max_keys) = ContractInfo::<Test>::deletion_budget(1, weight_limit);
|
||||
let vals: Vec<_> = (0..max_keys + extra_keys)
|
||||
.map(|i| (blake2_256(&i.encode()), (i as u32), (i as u32).encode()))
|
||||
.collect();
|
||||
@@ -2161,14 +2145,7 @@ fn lazy_removal_partial_remove_works() {
|
||||
|
||||
// Put value into the contracts child trie
|
||||
for val in &vals {
|
||||
Storage::<Test>::write(
|
||||
&info.trie_id,
|
||||
&val.0 as &FixSizedKey,
|
||||
Some(val.2.clone()),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
info.write(&val.0 as &FixSizedKey, Some(val.2.clone()), None, false).unwrap();
|
||||
}
|
||||
<ContractInfoOf<Test>>::insert(&addr, info.clone());
|
||||
|
||||
@@ -2201,7 +2178,7 @@ fn lazy_removal_partial_remove_works() {
|
||||
|
||||
ext.execute_with(|| {
|
||||
// Run the lazy removal
|
||||
let weight_used = Storage::<Test>::process_deletion_queue_batch(weight_limit);
|
||||
let weight_used = ContractInfo::<Test>::process_deletion_queue_batch(weight_limit);
|
||||
|
||||
// Weight should be exhausted because we could not even delete all keys
|
||||
assert_eq!(weight_used, weight_limit);
|
||||
@@ -2235,7 +2212,7 @@ fn lazy_removal_does_no_run_on_full_queue_and_full_block() {
|
||||
|
||||
// Fill the deletion queue with dummy values, so that on_initialize attempts
|
||||
// to clear the queue
|
||||
Storage::<Test>::fill_queue_with_dummies();
|
||||
ContractInfo::<Test>::fill_queue_with_dummies();
|
||||
|
||||
// Check that on_initialize() tries to perform lazy removal but removes nothing
|
||||
// as no more weight is left for that.
|
||||
@@ -2344,7 +2321,7 @@ fn lazy_removal_does_not_use_all_weight() {
|
||||
.account_id;
|
||||
|
||||
let info = get_contract(&addr);
|
||||
let (weight_per_key, max_keys) = Storage::<Test>::deletion_budget(1, weight_limit);
|
||||
let (weight_per_key, max_keys) = ContractInfo::<Test>::deletion_budget(1, weight_limit);
|
||||
|
||||
// We create a contract with one less storage item than we can remove within the limit
|
||||
let vals: Vec<_> = (0..max_keys - 1)
|
||||
@@ -2353,14 +2330,7 @@ fn lazy_removal_does_not_use_all_weight() {
|
||||
|
||||
// Put value into the contracts child trie
|
||||
for val in &vals {
|
||||
Storage::<Test>::write(
|
||||
&info.trie_id,
|
||||
&val.0 as &FixSizedKey,
|
||||
Some(val.2.clone()),
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.unwrap();
|
||||
info.write(&val.0 as &FixSizedKey, Some(val.2.clone()), None, false).unwrap();
|
||||
}
|
||||
<ContractInfoOf<Test>>::insert(&addr, info.clone());
|
||||
|
||||
@@ -2393,7 +2363,7 @@ fn lazy_removal_does_not_use_all_weight() {
|
||||
|
||||
ext.execute_with(|| {
|
||||
// Run the lazy removal
|
||||
let weight_used = Storage::<Test>::process_deletion_queue_batch(weight_limit);
|
||||
let weight_used = ContractInfo::<Test>::process_deletion_queue_batch(weight_limit);
|
||||
|
||||
// We have one less key in our trie than our weight limit suffices for
|
||||
assert_eq!(weight_used, weight_limit - weight_per_key);
|
||||
@@ -2427,7 +2397,7 @@ fn deletion_queue_full() {
|
||||
.account_id;
|
||||
|
||||
// fill the deletion queue up until its limit
|
||||
Storage::<Test>::fill_queue_with_dummies();
|
||||
ContractInfo::<Test>::fill_queue_with_dummies();
|
||||
|
||||
// Terminate the contract should fail
|
||||
assert_err_ignore_postinfo!(
|
||||
@@ -3196,14 +3166,12 @@ fn instantiate_with_zero_balance_works() {
|
||||
.account_id;
|
||||
|
||||
// Check that the BOB contract has been instantiated.
|
||||
get_contract(&addr);
|
||||
let contract = get_contract(&addr);
|
||||
let deposit_account = contract.deposit_account().deref();
|
||||
|
||||
// Make sure the account exists even though no free balance was send
|
||||
assert_eq!(<Test as Config>::Currency::free_balance(&addr), 0,);
|
||||
assert_eq!(
|
||||
<Test as Config>::Currency::total_balance(&addr),
|
||||
<Test as Config>::Currency::minimum_balance(),
|
||||
);
|
||||
assert_eq!(<Test as Config>::Currency::free_balance(&addr), min_balance);
|
||||
assert_eq!(<Test as Config>::Currency::total_balance(&addr), min_balance,);
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
@@ -3211,7 +3179,31 @@ fn instantiate_with_zero_balance_works() {
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
account: addr.clone()
|
||||
account: deposit_account.clone(),
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Endowed {
|
||||
account: deposit_account.clone(),
|
||||
free_balance: min_balance,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: ALICE,
|
||||
to: deposit_account.clone(),
|
||||
amount: min_balance,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
account: addr.clone(),
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
@@ -3232,14 +3224,6 @@ fn instantiate_with_zero_balance_works() {
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
|
||||
who: addr.clone(),
|
||||
amount: min_balance,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
|
||||
@@ -3292,18 +3276,40 @@ fn instantiate_with_below_existential_deposit_works() {
|
||||
.account_id;
|
||||
|
||||
// Check that the BOB contract has been instantiated.
|
||||
get_contract(&addr);
|
||||
let contract = get_contract(&addr);
|
||||
let deposit_account = contract.deposit_account().deref();
|
||||
|
||||
// Make sure the account exists even though no free balance was send
|
||||
assert_eq!(<Test as Config>::Currency::free_balance(&addr), 50,);
|
||||
assert_eq!(
|
||||
<Test as Config>::Currency::total_balance(&addr),
|
||||
<Test as Config>::Currency::minimum_balance() + 50,
|
||||
);
|
||||
// Make sure the account exists even though not enough free balance was send
|
||||
assert_eq!(<Test as Config>::Currency::free_balance(&addr), min_balance + 50);
|
||||
assert_eq!(<Test as Config>::Currency::total_balance(&addr), min_balance + 50);
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
account: deposit_account.clone(),
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Endowed {
|
||||
account: deposit_account.clone(),
|
||||
free_balance: min_balance,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: ALICE,
|
||||
to: deposit_account.clone(),
|
||||
amount: min_balance,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
@@ -3328,14 +3334,6 @@ fn instantiate_with_below_existential_deposit_works() {
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
|
||||
who: addr.clone(),
|
||||
amount: min_balance,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
@@ -3436,6 +3434,9 @@ fn storage_deposit_works() {
|
||||
deposit -= refunded0;
|
||||
assert_eq!(get_contract(&addr).total_deposit(), deposit);
|
||||
|
||||
let contract = get_contract(&addr);
|
||||
let deposit_account = contract.deposit_account().deref();
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![
|
||||
@@ -3460,15 +3461,7 @@ fn storage_deposit_works() {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: ALICE,
|
||||
to: addr.clone(),
|
||||
amount: charged0,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
|
||||
who: addr.clone(),
|
||||
to: deposit_account.clone(),
|
||||
amount: charged0,
|
||||
}),
|
||||
topics: vec![],
|
||||
@@ -3485,15 +3478,7 @@ fn storage_deposit_works() {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: ALICE,
|
||||
to: addr.clone(),
|
||||
amount: charged1,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Reserved {
|
||||
who: addr.clone(),
|
||||
to: deposit_account.clone(),
|
||||
amount: charged1,
|
||||
}),
|
||||
topics: vec![],
|
||||
@@ -3508,11 +3493,10 @@ fn storage_deposit_works() {
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::ReserveRepatriated {
|
||||
from: addr.clone(),
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: deposit_account.clone(),
|
||||
to: ALICE,
|
||||
amount: refunded0,
|
||||
destination_status: BalanceStatus::Free,
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
@@ -3610,11 +3594,10 @@ fn set_code_extrinsic() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn call_after_killed_account_needs_funding() {
|
||||
fn slash_cannot_kill_account() {
|
||||
let (wasm, _code_hash) = compile_module::<Test>("dummy").unwrap();
|
||||
ExtBuilder::default().existential_deposit(200).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
let min_balance = <Test as Config>::Currency::minimum_balance();
|
||||
|
||||
let addr = Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
@@ -3633,107 +3616,24 @@ fn call_after_killed_account_needs_funding() {
|
||||
// Drop previous events
|
||||
initialize_block(2);
|
||||
|
||||
// Destroy the account of the contract by slashing.
|
||||
// Slashing can actually happen if the contract takes part in staking.
|
||||
// It is a corner case and we accept the destruction of the account.
|
||||
// Try to destroy the account of the contract by slashing.
|
||||
// The account does not get destroyed because of the consumer reference.
|
||||
// Slashing can for example happen if the contract takes part in staking.
|
||||
let _ = <Test as Config>::Currency::slash(
|
||||
&addr,
|
||||
<Test as Config>::Currency::total_balance(&addr),
|
||||
);
|
||||
|
||||
// Sending below the minimum balance will fail the call because it needs to create the
|
||||
// account in order to send balance there.
|
||||
assert_err_ignore_postinfo!(
|
||||
Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr.clone(),
|
||||
min_balance - 1,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
vec![],
|
||||
),
|
||||
<Error<Test>>::TransferFailed
|
||||
);
|
||||
|
||||
// Sending zero should work as it does not do a transfer
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
vec![],
|
||||
));
|
||||
|
||||
// Sending minimum balance should work
|
||||
assert_ok!(Contracts::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr.clone(),
|
||||
min_balance,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
vec![],
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
System::events(),
|
||||
vec![
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::KilledAccount {
|
||||
account: addr.clone()
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Slashed {
|
||||
who: addr.clone(),
|
||||
amount: min_balance + 700
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Contracts(crate::Event::Called {
|
||||
caller: ALICE,
|
||||
contract: addr.clone(),
|
||||
}),
|
||||
topics: vec![hash(&ALICE), hash(&addr)],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::System(frame_system::Event::NewAccount {
|
||||
account: addr.clone()
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Endowed {
|
||||
account: addr.clone(),
|
||||
free_balance: min_balance
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Transfer {
|
||||
from: ALICE,
|
||||
to: addr.clone(),
|
||||
amount: min_balance
|
||||
}),
|
||||
topics: vec![],
|
||||
},
|
||||
EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Contracts(crate::Event::Called {
|
||||
caller: ALICE,
|
||||
contract: addr.clone(),
|
||||
}),
|
||||
topics: vec![hash(&ALICE), hash(&addr)],
|
||||
},
|
||||
]
|
||||
vec![EventRecord {
|
||||
phase: Phase::Initialization,
|
||||
event: RuntimeEvent::Balances(pallet_balances::Event::Slashed {
|
||||
who: addr.clone(),
|
||||
amount: 700, // slash didn't remove the minimum balance
|
||||
}),
|
||||
topics: vec![],
|
||||
},]
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
+1467
-1463
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user