mirror of
https://github.com/pezkuwichain/pezkuwi-subxt.git
synced 2026-05-30 22:11:02 +00:00
Contracts: runtime_call and storage_deposit (#13990)
* wip * add comments * fix comment * comments * comments * PR comment * field orders * Update frame/contracts/src/tests.rs * Update frame/contracts/fixtures/call_runtime_and_call.wat Co-authored-by: Sasha Gryaznov <hi@agryaznov.com> * Apply suggestions from code review Co-authored-by: Sasha Gryaznov <hi@agryaznov.com> * Apply suggestions from code review Co-authored-by: Sasha Gryaznov <hi@agryaznov.com> * Update frame/contracts/src/tests.rs Co-authored-by: Sasha Gryaznov <hi@agryaznov.com> * Validate fees of failed call * Update frame/contracts/src/tests.rs * Update frame/contracts/src/tests.rs * Update frame/contracts/src/tests.rs * bubble up refund error * rename fixture file --------- Co-authored-by: Sasha Gryaznov <hi@agryaznov.com> Co-authored-by: parity-processbot <>
This commit is contained in:
@@ -1044,7 +1044,15 @@ impl<T: Config> Invokable<T> for CallInput<T> {
|
||||
debug_message,
|
||||
*determinism,
|
||||
);
|
||||
InternalOutput { gas_meter, storage_deposit: storage_meter.into_deposit(&origin), result }
|
||||
|
||||
match storage_meter.try_into_deposit(&origin) {
|
||||
Ok(storage_deposit) => InternalOutput { gas_meter, storage_deposit, result },
|
||||
Err(err) => InternalOutput {
|
||||
gas_meter,
|
||||
storage_deposit: Default::default(),
|
||||
result: Err(err.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1105,8 +1113,9 @@ impl<T: Config> Invokable<T> for InstantiateInput<T> {
|
||||
&salt,
|
||||
debug_message,
|
||||
);
|
||||
|
||||
storage_deposit = storage_meter
|
||||
.into_deposit(&origin)
|
||||
.try_into_deposit(&origin)?
|
||||
.saturating_add(&StorageDeposit::Charge(extra_deposit));
|
||||
result
|
||||
};
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
use crate::{
|
||||
storage::{ContractInfo, DepositAccount},
|
||||
BalanceOf, Config, Error, Inspect, Pallet, System, LOG_TARGET,
|
||||
BalanceOf, Config, Error, Inspect, Pallet, System,
|
||||
};
|
||||
use codec::Encode;
|
||||
use frame_support::{
|
||||
@@ -85,7 +85,7 @@ pub trait Ext<T: Config> {
|
||||
deposit_account: &DepositAccount<T>,
|
||||
amount: &DepositOf<T>,
|
||||
terminated: bool,
|
||||
);
|
||||
) -> Result<(), DispatchError>;
|
||||
}
|
||||
|
||||
/// This [`Ext`] is used for actual on-chain execution when balance needs to be charged.
|
||||
@@ -366,14 +366,14 @@ where
|
||||
///
|
||||
/// This drops the root meter in order to make sure it is only called when the whole
|
||||
/// execution did finish.
|
||||
pub fn into_deposit(self, origin: &T::AccountId) -> DepositOf<T> {
|
||||
pub fn try_into_deposit(self, origin: &T::AccountId) -> Result<DepositOf<T>, DispatchError> {
|
||||
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Refund(_))) {
|
||||
E::charge(origin, &charge.deposit_account, &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.deposit_account, &charge.amount, charge.terminated);
|
||||
E::charge(origin, &charge.deposit_account, &charge.amount, charge.terminated)?;
|
||||
}
|
||||
self.total_deposit
|
||||
Ok(self.total_deposit)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -428,7 +428,8 @@ where
|
||||
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.
|
||||
@@ -514,71 +515,27 @@ impl<T: Config> Ext<T> for ReservingExt {
|
||||
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.
|
||||
// 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.
|
||||
) -> Result<(), DispatchError> {
|
||||
match amount {
|
||||
Deposit::Charge(amount) => {
|
||||
// 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,
|
||||
deposit_account,
|
||||
*amount,
|
||||
ExistenceRequirement::KeepAlive,
|
||||
);
|
||||
if let Err(err) = result {
|
||||
log::error!(
|
||||
target: LOG_TARGET,
|
||||
"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.");
|
||||
}
|
||||
}
|
||||
},
|
||||
// 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. No one has access to the deposit account. Hence no
|
||||
// other interaction with this account takes place.
|
||||
Deposit::Charge(amount) => T::Currency::transfer(
|
||||
origin,
|
||||
deposit_account,
|
||||
*amount,
|
||||
ExistenceRequirement::KeepAlive,
|
||||
),
|
||||
Deposit::Refund(amount) => {
|
||||
if terminated {
|
||||
System::<T>::dec_consumers(&deposit_account);
|
||||
}
|
||||
let result = T::Currency::transfer(
|
||||
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: LOG_TARGET,
|
||||
"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.");
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -651,7 +608,7 @@ mod tests {
|
||||
contract: &DepositAccount<Test>,
|
||||
amount: &DepositOf<Test>,
|
||||
terminated: bool,
|
||||
) {
|
||||
) -> Result<(), DispatchError> {
|
||||
TestExtTestValue::mutate(|ext| {
|
||||
ext.charges.push(Charge {
|
||||
origin: origin.clone(),
|
||||
@@ -660,6 +617,7 @@ mod tests {
|
||||
terminated,
|
||||
})
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -757,7 +715,7 @@ mod tests {
|
||||
nested0.enforce_limit(Some(&mut nested0_info)).unwrap();
|
||||
meter.absorb(nested0, DepositAccount(BOB), Some(&mut nested0_info));
|
||||
|
||||
meter.into_deposit(&ALICE);
|
||||
meter.try_into_deposit(&ALICE).unwrap();
|
||||
|
||||
assert_eq!(nested0_info.extra_deposit(), 112);
|
||||
assert_eq!(nested1_info.extra_deposit(), 110);
|
||||
@@ -817,7 +775,7 @@ mod tests {
|
||||
nested0.absorb(nested1, DepositAccount(CHARLIE), None);
|
||||
|
||||
meter.absorb(nested0, DepositAccount(BOB), None);
|
||||
meter.into_deposit(&ALICE);
|
||||
meter.try_into_deposit(&ALICE).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
TestExtTestValue::get(),
|
||||
|
||||
@@ -33,7 +33,7 @@ use crate::{
|
||||
use assert_matches::assert_matches;
|
||||
use codec::Encode;
|
||||
use frame_support::{
|
||||
assert_err, assert_err_ignore_postinfo, assert_noop, assert_ok,
|
||||
assert_err, assert_err_ignore_postinfo, assert_err_with_weight, assert_noop, assert_ok,
|
||||
dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo},
|
||||
parameter_types,
|
||||
storage::child,
|
||||
@@ -70,6 +70,7 @@ frame_support::construct_runtime!(
|
||||
Randomness: pallet_insecure_randomness_collective_flip::{Pallet, Storage},
|
||||
Utility: pallet_utility::{Pallet, Call, Storage, Event},
|
||||
Contracts: pallet_contracts::{Pallet, Call, Storage, Event<T>},
|
||||
Proxy: pallet_proxy::{Pallet, Call, Storage, Event<T>},
|
||||
}
|
||||
);
|
||||
|
||||
@@ -337,6 +338,22 @@ impl pallet_utility::Config for Test {
|
||||
type PalletsOrigin = OriginCaller;
|
||||
type WeightInfo = ();
|
||||
}
|
||||
|
||||
impl pallet_proxy::Config for Test {
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
type RuntimeCall = RuntimeCall;
|
||||
type Currency = Balances;
|
||||
type ProxyType = ();
|
||||
type ProxyDepositBase = ConstU64<1>;
|
||||
type ProxyDepositFactor = ConstU64<1>;
|
||||
type MaxProxies = ConstU32<32>;
|
||||
type WeightInfo = ();
|
||||
type MaxPending = ConstU32<32>;
|
||||
type CallHasher = BlakeTwo256;
|
||||
type AnnouncementDepositBase = ConstU64<1>;
|
||||
type AnnouncementDepositFactor = ConstU64<1>;
|
||||
}
|
||||
|
||||
parameter_types! {
|
||||
pub MySchedule: Schedule<Test> = {
|
||||
let mut schedule = <Schedule<Test>>::default();
|
||||
@@ -2983,6 +3000,86 @@ fn sr25519_verify() {
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failed_deposit_charge_should_roll_back_call() {
|
||||
let (wasm_caller, _) = compile_module::<Test>("call_runtime_and_call").unwrap();
|
||||
let (wasm_callee, _) = compile_module::<Test>("store_call").unwrap();
|
||||
const ED: u64 = 200;
|
||||
|
||||
let execute = || {
|
||||
ExtBuilder::default().existential_deposit(ED).build().execute_with(|| {
|
||||
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
|
||||
|
||||
// Instantiate both contracts.
|
||||
let addr_caller = Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
Code::Upload(wasm_caller.clone()),
|
||||
vec![],
|
||||
vec![],
|
||||
false,
|
||||
)
|
||||
.result
|
||||
.unwrap()
|
||||
.account_id;
|
||||
let addr_callee = Contracts::bare_instantiate(
|
||||
ALICE,
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
Code::Upload(wasm_callee.clone()),
|
||||
vec![],
|
||||
vec![],
|
||||
false,
|
||||
)
|
||||
.result
|
||||
.unwrap()
|
||||
.account_id;
|
||||
|
||||
// Give caller proxy access to Alice.
|
||||
assert_ok!(Proxy::add_proxy(RuntimeOrigin::signed(ALICE), addr_caller.clone(), (), 0));
|
||||
|
||||
// Create a Proxy call that will attempt to transfer away Alice's balance.
|
||||
let transfer_call =
|
||||
Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death {
|
||||
dest: CHARLIE,
|
||||
value: pallet_balances::Pallet::<Test>::free_balance(&ALICE) - 2 * ED,
|
||||
}));
|
||||
|
||||
// Wrap the transfer call in a proxy call.
|
||||
let transfer_proxy_call = RuntimeCall::Proxy(pallet_proxy::Call::proxy {
|
||||
real: ALICE,
|
||||
force_proxy_type: Some(()),
|
||||
call: transfer_call,
|
||||
});
|
||||
|
||||
let data = (
|
||||
(ED - DepositPerItem::get()) as u32, // storage length
|
||||
addr_callee,
|
||||
transfer_proxy_call,
|
||||
);
|
||||
|
||||
<Pallet<Test>>::call(
|
||||
RuntimeOrigin::signed(ALICE),
|
||||
addr_caller.clone(),
|
||||
0,
|
||||
GAS_LIMIT,
|
||||
None,
|
||||
data.encode(),
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
// With a low enough deposit per byte, the call should succeed.
|
||||
let result = execute().unwrap();
|
||||
|
||||
// Bump the deposit per byte to a high value to trigger a FundsUnavailable error.
|
||||
DEPOSIT_PER_BYTE.with(|c| *c.borrow_mut() = ED);
|
||||
assert_err_with_weight!(execute(), TokenError::FundsUnavailable, result.actual_weight);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn upload_code_works() {
|
||||
let (wasm, code_hash) = compile_module::<Test>("dummy").unwrap();
|
||||
|
||||
Reference in New Issue
Block a user