Skip to content
This repository was archived by the owner on Nov 15, 2023. It is now read-only.

Commit bc8f2db

Browse files
pgherveouagryaznov
andauthored
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 <[email protected]> * Apply suggestions from code review Co-authored-by: Sasha Gryaznov <[email protected]> * Apply suggestions from code review Co-authored-by: Sasha Gryaznov <[email protected]> * Update frame/contracts/src/tests.rs Co-authored-by: Sasha Gryaznov <[email protected]> * 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 <[email protected]> Co-authored-by: parity-processbot <>
1 parent 74b2c92 commit bc8f2db

File tree

6 files changed

+189
-67
lines changed

6 files changed

+189
-67
lines changed

Diff for: Cargo.lock

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Diff for: frame/contracts/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ pallet-balances = { version = "4.0.0-dev", path = "../balances" }
5959
pallet-timestamp = { version = "4.0.0-dev", path = "../timestamp" }
6060
pallet-insecure-randomness-collective-flip = { version = "4.0.0-dev", path = "../insecure-randomness-collective-flip" }
6161
pallet-utility = { version = "4.0.0-dev", path = "../utility" }
62+
pallet-proxy = { version = "4.0.0-dev", path = "../proxy" }
6263
sp-keystore = { version = "0.13.0", path = "../../primitives/keystore" }
6364

6465
[features]

Diff for: frame/contracts/fixtures/call_runtime_and_call.wat

+56
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
(module
2+
(import "seal0" "call_runtime" (func $call_runtime (param i32 i32) (result i32)))
3+
(import "seal1" "seal_call" (func $seal_call (param i32 i32 i64 i32 i32 i32 i32 i32) (result i32)))
4+
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
5+
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
6+
(import "env" "memory" (memory 1 1))
7+
8+
(func $assert (param i32)
9+
(block $ok
10+
(br_if $ok (get_local 0))
11+
(unreachable)
12+
)
13+
)
14+
15+
(func (export "call")
16+
;; Store available input size at offset 0.
17+
(i32.store (i32.const 0) (i32.const 512))
18+
19+
;; read input data
20+
(call $seal_input (i32.const 4) (i32.const 0))
21+
22+
;; Input data layout.
23+
;; [0..4) - size of the call
24+
;; [4..8) - how many bytes to add to storage
25+
;; [8..40) - address of the callee
26+
;; [40..n) - encoded runtime call
27+
28+
;; Invoke call_runtime with the encoded call passed to this contract.
29+
(call $assert (i32.eqz
30+
(call $call_runtime
31+
(i32.const 40) ;; Pointer where the call is stored
32+
(i32.sub
33+
(i32.load (i32.const 0)) ;; Size of the call
34+
(i32.const 36) ;; Subtract size of the subcall-related part: 4 bytes for storage length to add + 32 bytes of the callee address
35+
)
36+
)
37+
))
38+
39+
;; call passed contract
40+
(call $assert (i32.eqz
41+
(call $seal_call
42+
(i32.const 0) ;; No flags
43+
(i32.const 8) ;; Pointer to "callee" address.
44+
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
45+
(i32.const 512) ;; Pointer to the buffer with value to transfer
46+
(i32.const 4) ;; Pointer to input data buffer address
47+
(i32.const 4) ;; Length of input data buffer
48+
(i32.const 4294967295) ;; u32 max value is the sentinel value: do not copy output
49+
(i32.const 0) ;; Length is ignored in this case
50+
)
51+
))
52+
)
53+
54+
(func (export "deploy"))
55+
)
56+

Diff for: frame/contracts/src/lib.rs

+11-2
Original file line numberDiff line numberDiff line change
@@ -1044,7 +1044,15 @@ impl<T: Config> Invokable<T> for CallInput<T> {
10441044
debug_message,
10451045
*determinism,
10461046
);
1047-
InternalOutput { gas_meter, storage_deposit: storage_meter.into_deposit(&origin), result }
1047+
1048+
match storage_meter.try_into_deposit(&origin) {
1049+
Ok(storage_deposit) => InternalOutput { gas_meter, storage_deposit, result },
1050+
Err(err) => InternalOutput {
1051+
gas_meter,
1052+
storage_deposit: Default::default(),
1053+
result: Err(err.into()),
1054+
},
1055+
}
10481056
}
10491057
}
10501058

@@ -1105,8 +1113,9 @@ impl<T: Config> Invokable<T> for InstantiateInput<T> {
11051113
&salt,
11061114
debug_message,
11071115
);
1116+
11081117
storage_deposit = storage_meter
1109-
.into_deposit(&origin)
1118+
.try_into_deposit(&origin)?
11101119
.saturating_add(&StorageDeposit::Charge(extra_deposit));
11111120
result
11121121
};

Diff for: frame/contracts/src/storage/meter.rs

+22-64
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
2020
use crate::{
2121
storage::{ContractInfo, DepositAccount},
22-
BalanceOf, Config, Error, Inspect, Pallet, System, LOG_TARGET,
22+
BalanceOf, Config, Error, Inspect, Pallet, System,
2323
};
2424
use codec::Encode;
2525
use frame_support::{
@@ -85,7 +85,7 @@ pub trait Ext<T: Config> {
8585
deposit_account: &DepositAccount<T>,
8686
amount: &DepositOf<T>,
8787
terminated: bool,
88-
);
88+
) -> Result<(), DispatchError>;
8989
}
9090

9191
/// This [`Ext`] is used for actual on-chain execution when balance needs to be charged.
@@ -366,14 +366,14 @@ where
366366
///
367367
/// This drops the root meter in order to make sure it is only called when the whole
368368
/// execution did finish.
369-
pub fn into_deposit(self, origin: &T::AccountId) -> DepositOf<T> {
369+
pub fn try_into_deposit(self, origin: &T::AccountId) -> Result<DepositOf<T>, DispatchError> {
370370
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Refund(_))) {
371-
E::charge(origin, &charge.deposit_account, &charge.amount, charge.terminated);
371+
E::charge(origin, &charge.deposit_account, &charge.amount, charge.terminated)?;
372372
}
373373
for charge in self.charges.iter().filter(|c| matches!(c.amount, Deposit::Charge(_))) {
374-
E::charge(origin, &charge.deposit_account, &charge.amount, charge.terminated);
374+
E::charge(origin, &charge.deposit_account, &charge.amount, charge.terminated)?;
375375
}
376-
self.total_deposit
376+
Ok(self.total_deposit)
377377
}
378378
}
379379

@@ -428,7 +428,8 @@ where
428428
info.deposit_account(),
429429
&deposit.saturating_sub(&Deposit::Charge(ed)),
430430
false,
431-
);
431+
)?;
432+
432433
System::<T>::inc_consumers(info.deposit_account())?;
433434

434435
// We also need to make sure that the contract's account itself exists.
@@ -514,71 +515,27 @@ impl<T: Config> Ext<T> for ReservingExt {
514515
deposit_account: &DepositAccount<T>,
515516
amount: &DepositOf<T>,
516517
terminated: bool,
517-
) {
518-
// There is nothing we can do when this fails as this constitutes a bug in the runtime.
519-
// We need to settle for emitting an error log in this case.
520-
//
521-
// # Note
522-
//
523-
// This is infallible because it is called in a part of the execution where we cannot
524-
// simply roll back. It might make sense to do some refactoring to move the deposit
525-
// collection to the fallible part of execution.
518+
) -> Result<(), DispatchError> {
526519
match amount {
527-
Deposit::Charge(amount) => {
528-
// This will never fail because a deposit account is required to exist
529-
// at all times. The pallet enforces this invariant by holding a consumer reference
530-
// on the deposit account as long as the contract exists.
531-
//
532-
// The sender always has enough balance because we checked that it had enough
533-
// balance when instantiating the storage meter. There is no way for the sender
534-
// which is a plain account to send away this balance in the meantime.
535-
let result = T::Currency::transfer(
536-
origin,
537-
deposit_account,
538-
*amount,
539-
ExistenceRequirement::KeepAlive,
540-
);
541-
if let Err(err) = result {
542-
log::error!(
543-
target: LOG_TARGET,
544-
"Failed to transfer storage deposit {:?} from origin {:?} to deposit account {:?}: {:?}",
545-
amount, origin, deposit_account, err,
546-
);
547-
if cfg!(debug_assertions) {
548-
panic!("Unable to collect storage deposit. This is a bug.");
549-
}
550-
}
551-
},
552-
// The receiver always exists because the initial value transfer from the
553-
// origin to the contract has a keep alive existence requirement. When taking a deposit
554-
// we make sure to leave at least the ed in the free balance.
555-
//
556-
// The sender always has enough balance because we track it in the `ContractInfo` and
557-
// never send more back than we have. No one has access to the deposit account. Hence no
558-
// other interaction with this account takes place.
520+
Deposit::Charge(amount) => T::Currency::transfer(
521+
origin,
522+
deposit_account,
523+
*amount,
524+
ExistenceRequirement::KeepAlive,
525+
),
559526
Deposit::Refund(amount) => {
560527
if terminated {
561528
System::<T>::dec_consumers(&deposit_account);
562529
}
563-
let result = T::Currency::transfer(
530+
T::Currency::transfer(
564531
deposit_account,
565532
origin,
566533
*amount,
567534
// We can safely use `AllowDeath` because our own consumer prevents an removal.
568535
ExistenceRequirement::AllowDeath,
569-
);
570-
if matches!(result, Err(_)) {
571-
log::error!(
572-
target: LOG_TARGET,
573-
"Failed to refund storage deposit {:?} from deposit account {:?} to origin {:?}: {:?}",
574-
amount, deposit_account, origin, result,
575-
);
576-
if cfg!(debug_assertions) {
577-
panic!("Unable to refund storage deposit. This is a bug.");
578-
}
579-
}
536+
)
580537
},
581-
};
538+
}
582539
}
583540
}
584541

@@ -651,7 +608,7 @@ mod tests {
651608
contract: &DepositAccount<Test>,
652609
amount: &DepositOf<Test>,
653610
terminated: bool,
654-
) {
611+
) -> Result<(), DispatchError> {
655612
TestExtTestValue::mutate(|ext| {
656613
ext.charges.push(Charge {
657614
origin: origin.clone(),
@@ -660,6 +617,7 @@ mod tests {
660617
terminated,
661618
})
662619
});
620+
Ok(())
663621
}
664622
}
665623

@@ -757,7 +715,7 @@ mod tests {
757715
nested0.enforce_limit(Some(&mut nested0_info)).unwrap();
758716
meter.absorb(nested0, DepositAccount(BOB), Some(&mut nested0_info));
759717

760-
meter.into_deposit(&ALICE);
718+
meter.try_into_deposit(&ALICE).unwrap();
761719

762720
assert_eq!(nested0_info.extra_deposit(), 112);
763721
assert_eq!(nested1_info.extra_deposit(), 110);
@@ -817,7 +775,7 @@ mod tests {
817775
nested0.absorb(nested1, DepositAccount(CHARLIE), None);
818776

819777
meter.absorb(nested0, DepositAccount(BOB), None);
820-
meter.into_deposit(&ALICE);
778+
meter.try_into_deposit(&ALICE).unwrap();
821779

822780
assert_eq!(
823781
TestExtTestValue::get(),

Diff for: frame/contracts/src/tests.rs

+98-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ use crate::{
3333
use assert_matches::assert_matches;
3434
use codec::Encode;
3535
use frame_support::{
36-
assert_err, assert_err_ignore_postinfo, assert_noop, assert_ok,
36+
assert_err, assert_err_ignore_postinfo, assert_err_with_weight, assert_noop, assert_ok,
3737
dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo},
3838
parameter_types,
3939
storage::child,
@@ -70,6 +70,7 @@ frame_support::construct_runtime!(
7070
Randomness: pallet_insecure_randomness_collective_flip::{Pallet, Storage},
7171
Utility: pallet_utility::{Pallet, Call, Storage, Event},
7272
Contracts: pallet_contracts::{Pallet, Call, Storage, Event<T>},
73+
Proxy: pallet_proxy::{Pallet, Call, Storage, Event<T>},
7374
}
7475
);
7576

@@ -337,6 +338,22 @@ impl pallet_utility::Config for Test {
337338
type PalletsOrigin = OriginCaller;
338339
type WeightInfo = ();
339340
}
341+
342+
impl pallet_proxy::Config for Test {
343+
type RuntimeEvent = RuntimeEvent;
344+
type RuntimeCall = RuntimeCall;
345+
type Currency = Balances;
346+
type ProxyType = ();
347+
type ProxyDepositBase = ConstU64<1>;
348+
type ProxyDepositFactor = ConstU64<1>;
349+
type MaxProxies = ConstU32<32>;
350+
type WeightInfo = ();
351+
type MaxPending = ConstU32<32>;
352+
type CallHasher = BlakeTwo256;
353+
type AnnouncementDepositBase = ConstU64<1>;
354+
type AnnouncementDepositFactor = ConstU64<1>;
355+
}
356+
340357
parameter_types! {
341358
pub MySchedule: Schedule<Test> = {
342359
let mut schedule = <Schedule<Test>>::default();
@@ -2983,6 +3000,86 @@ fn sr25519_verify() {
29833000
})
29843001
}
29853002

3003+
#[test]
3004+
fn failed_deposit_charge_should_roll_back_call() {
3005+
let (wasm_caller, _) = compile_module::<Test>("call_runtime_and_call").unwrap();
3006+
let (wasm_callee, _) = compile_module::<Test>("store_call").unwrap();
3007+
const ED: u64 = 200;
3008+
3009+
let execute = || {
3010+
ExtBuilder::default().existential_deposit(ED).build().execute_with(|| {
3011+
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
3012+
3013+
// Instantiate both contracts.
3014+
let addr_caller = Contracts::bare_instantiate(
3015+
ALICE,
3016+
0,
3017+
GAS_LIMIT,
3018+
None,
3019+
Code::Upload(wasm_caller.clone()),
3020+
vec![],
3021+
vec![],
3022+
false,
3023+
)
3024+
.result
3025+
.unwrap()
3026+
.account_id;
3027+
let addr_callee = Contracts::bare_instantiate(
3028+
ALICE,
3029+
0,
3030+
GAS_LIMIT,
3031+
None,
3032+
Code::Upload(wasm_callee.clone()),
3033+
vec![],
3034+
vec![],
3035+
false,
3036+
)
3037+
.result
3038+
.unwrap()
3039+
.account_id;
3040+
3041+
// Give caller proxy access to Alice.
3042+
assert_ok!(Proxy::add_proxy(RuntimeOrigin::signed(ALICE), addr_caller.clone(), (), 0));
3043+
3044+
// Create a Proxy call that will attempt to transfer away Alice's balance.
3045+
let transfer_call =
3046+
Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death {
3047+
dest: CHARLIE,
3048+
value: pallet_balances::Pallet::<Test>::free_balance(&ALICE) - 2 * ED,
3049+
}));
3050+
3051+
// Wrap the transfer call in a proxy call.
3052+
let transfer_proxy_call = RuntimeCall::Proxy(pallet_proxy::Call::proxy {
3053+
real: ALICE,
3054+
force_proxy_type: Some(()),
3055+
call: transfer_call,
3056+
});
3057+
3058+
let data = (
3059+
(ED - DepositPerItem::get()) as u32, // storage length
3060+
addr_callee,
3061+
transfer_proxy_call,
3062+
);
3063+
3064+
<Pallet<Test>>::call(
3065+
RuntimeOrigin::signed(ALICE),
3066+
addr_caller.clone(),
3067+
0,
3068+
GAS_LIMIT,
3069+
None,
3070+
data.encode(),
3071+
)
3072+
})
3073+
};
3074+
3075+
// With a low enough deposit per byte, the call should succeed.
3076+
let result = execute().unwrap();
3077+
3078+
// Bump the deposit per byte to a high value to trigger a FundsUnavailable error.
3079+
DEPOSIT_PER_BYTE.with(|c| *c.borrow_mut() = ED);
3080+
assert_err_with_weight!(execute(), TokenError::FundsUnavailable, result.actual_weight);
3081+
}
3082+
29863083
#[test]
29873084
fn upload_code_works() {
29883085
let (wasm, code_hash) = compile_module::<Test>("dummy").unwrap();

0 commit comments

Comments
 (0)