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:
Alexander Theißen
2023-02-16 16:58:28 +01:00
committed by GitHub
parent 292e5ee4e7
commit 330484bb19
11 changed files with 2109 additions and 2184 deletions
@@ -16,27 +16,11 @@
)
)
(func $current_balance (param $sp i32) (result i64)
(i32.store
(i32.sub (get_local $sp) (i32.const 16))
(i32.const 8)
)
(call $seal_balance
(i32.sub (get_local $sp) (i32.const 8))
(i32.sub (get_local $sp) (i32.const 16))
)
(call $assert
(i32.eq (i32.load (i32.sub (get_local $sp) (i32.const 16))) (i32.const 8))
)
(i64.load (i32.sub (get_local $sp) (i32.const 8)))
)
(func (export "deploy"))
(func (export "call")
(local $sp i32)
(local $exit_code i32)
(local $balance i64)
;; Length of the buffer
(i32.store (i32.const 20) (i32.const 32))
@@ -54,9 +38,6 @@
;; Read current balance into local variable.
(set_local $sp (i32.const 1024))
(set_local $balance
(call $current_balance (get_local $sp))
)
;; Fail to deploy the contract since it returns a non-zero exit status.
(set_local $exit_code
@@ -82,11 +63,6 @@
(i32.eq (get_local $exit_code) (i32.const 2)) ;; ReturnCode::CalleeReverted
)
;; Check that balance has not changed.
(call $assert
(i64.eq (get_local $balance) (call $current_balance (get_local $sp)))
)
;; Fail to deploy the contract due to insufficient gas.
(set_local $exit_code
(call $seal_instantiate
@@ -112,11 +88,6 @@
(i32.eq (get_local $exit_code) (i32.const 1)) ;; ReturnCode::CalleeTrapped
)
;; Check that balance has not changed.
(call $assert
(i64.eq (get_local $balance) (call $current_balance (get_local $sp)))
)
;; Length of the output buffer
(i32.store
(i32.sub (get_local $sp) (i32.const 4))
@@ -153,14 +124,6 @@
(i32.eq (i32.load (i32.sub (get_local $sp) (i32.const 4))) (i32.const 32))
)
;; Check that balance has been deducted.
(set_local $balance
(i64.sub (get_local $balance) (i64.load (i32.const 0)))
)
(call $assert
(i64.eq (get_local $balance) (call $current_balance (get_local $sp)))
)
;; Zero out destination buffer of output
(i32.store
(i32.sub (get_local $sp) (i32.const 4))
@@ -204,11 +167,6 @@
)
)
;; Check that balance has not changed.
(call $assert
(i64.eq (get_local $balance) (call $current_balance (get_local $sp)))
)
;; Fail to call the contract due to insufficient gas.
(set_local $exit_code
(call $seal_call
@@ -229,11 +187,6 @@
(i32.eq (get_local $exit_code) (i32.const 1)) ;; ReturnCode::CalleeTrapped
)
;; Check that balance has not changed.
(call $assert
(i64.eq (get_local $balance) (call $current_balance (get_local $sp)))
)
;; Zero out destination buffer of output
(i32.store
(i32.sub (get_local $sp) (i32.const 4))
@@ -276,14 +229,6 @@
(i32.const 0x77665544)
)
)
;; Check that balance has been deducted.
(set_local $balance
(i64.sub (get_local $balance) (i64.load (i32.const 0)))
)
(call $assert
(i64.eq (get_local $balance) (call $current_balance (get_local $sp)))
)
)
(data (i32.const 0) "\00\80") ;; The value to transfer on instantiation and calls.
+2 -3
View File
@@ -34,8 +34,7 @@
)
;; Try to self-destruct by sending full balance to the 0 address.
;; All the *free* balance will be send away, which is a valid thing to do
;; because the storage deposits will keep the account alive.
;; The call will fail because a contract transfer has a keep alive requirement
(call $assert
(i32.eq
(call $seal_transfer
@@ -44,7 +43,7 @@
(i32.const 0) ;; Pointer to the buffer with value to transfer
(i32.const 8) ;; Length of the buffer with value to transfer
)
(i32.const 0) ;; ReturnCode::Success
(i32.const 5) ;; ReturnCode::TransferFailed
)
)
)
@@ -47,10 +47,12 @@ pub struct ContractResult<R, Balance> {
/// Additionally, any `seal_call` or `seal_instantiate` makes use of pre-charging
/// when a non-zero `gas_limit` argument is supplied.
pub gas_required: Weight,
/// How much balance was deposited and reserved during execution in order to pay for storage.
/// How much balance was paid by the origin into the contract's deposit account in order to
/// pay for storage.
///
/// The storage deposit is never actually charged from the caller in case of [`Self::result`]
/// is `Err`. This is because on error all storage changes are rolled back.
/// The storage deposit is never actually charged from the origin in case of [`Self::result`]
/// is `Err`. This is because on error all storage changes are rolled back including the
/// payment of the deposit.
pub storage_deposit: StorageDeposit<Balance>,
/// An optional debug message. This message is only filled when explicitly requested
/// by the code that calls into the contract. Otherwise it is empty.
@@ -159,12 +161,12 @@ pub enum StorageDeposit<Balance> {
/// The transaction reduced storage consumption.
///
/// This means that the specified amount of balance was transferred from the involved
/// contracts to the call origin.
/// deposit accounts to the origin.
Refund(Balance),
/// The transaction increased overall storage usage.
/// The transaction increased storage consumption.
///
/// This means that the specified amount of balance was transferred from the call origin
/// to the contracts involved.
/// This means that the specified amount of balance was transferred from the origin
/// to the involved deposit accounts.
Charge(Balance),
}
+81
View File
@@ -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);
+58 -39
View File
@@ -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,
+10 -52
View File
@@ -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.
+128 -108
View File
@@ -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
}
}
+103 -90
View File
@@ -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
}
+209 -309
View File
@@ -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![],
},]
);
});
}
File diff suppressed because it is too large Load Diff