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

Contracts: runtime_call and storage_deposit #13990

Merged
merged 21 commits into from
Apr 29, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions frame/contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ pallet-balances = { version = "4.0.0-dev", path = "../balances" }
pallet-timestamp = { version = "4.0.0-dev", path = "../timestamp" }
pallet-insecure-randomness-collective-flip = { version = "4.0.0-dev", path = "../insecure-randomness-collective-flip" }
pallet-utility = { version = "4.0.0-dev", path = "../utility" }
pallet-proxy = { version = "4.0.0-dev", path = "../proxy" }
sp-keystore = { version = "0.13.0", path = "../../primitives/keystore" }

[features]
Expand Down
56 changes: 56 additions & 0 deletions frame/contracts/fixtures/call_runtime_and_call.wat
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
(module
(import "seal0" "call_runtime" (func $call_runtime (param i32 i32) (result i32)))
(import "seal1" "seal_call" (func $seal_call (param i32 i32 i64 i32 i32 i32 i32 i32) (result i32)))
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "env" "memory" (memory 1 1))

(func $assert (param i32)
(block $ok
(br_if $ok (get_local 0))
(unreachable)
)
)

(func (export "call")
;; Store available input size at offset 0.
(i32.store (i32.const 0) (i32.const 512))

;; read input data
(call $seal_input (i32.const 4) (i32.const 0))

;; Input data layout.
;; [0..4) - size of the call
;; [4..8) - storage length
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
;; [8..40) - hash code of the callee
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
;; [40..n) - encoded runtime call

;; Invoke call_runtime with the encoded call passed to this contract.
(call $assert (i32.eqz
(call $call_runtime
(i32.const 40) ;; Pointer where the call is stored
(i32.sub
(i32.load (i32.const 0)) ;; Size of the call
(i32.const 36) ;; Size of the call without the call_runtime
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
)
)
))

;; call passed contract
(call $assert (i32.eqz
(call $seal_call
(i32.const 0) ;; No flags
(i32.const 8) ;; Pointer to "callee" address.
(i64.const 0) ;; How much gas to devote for the execution. 0 = all.
(i32.const 512) ;; Pointer to the buffer with value to transfer
(i32.const 4) ;; Pointer to input data buffer address
(i32.const 4) ;; Length of input data buffer
(i32.const 4294967295) ;; u32 max value is the sentinel value: do not copy output
(i32.const 0) ;; Length is ignored in this case
)
))
)

(func (export "deploy"))
)

13 changes: 11 additions & 2 deletions frame/contracts/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1033,7 +1033,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, result, storage_deposit },
Err(err) => InternalOutput {
result: Err(err.into()),
gas_meter,
storage_deposit: Default::default(),
},
}
}
}

Expand Down Expand Up @@ -1094,8 +1102,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));
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
result
};
Expand Down
64 changes: 20 additions & 44 deletions frame/contracts/src/storage/meter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,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.
Expand Down Expand Up @@ -343,14 +343,14 @@ where
///
Copy link
Contributor

@agryaznov agryaznov Apr 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

docs above this line could be improved a bit to reflect the fact this is now a fallible fn

/// 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)
}
}

Expand Down Expand Up @@ -405,7 +405,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.
Expand Down Expand Up @@ -476,41 +477,14 @@ 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: "runtime::contracts",
"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.");
}
}
},
Deposit::Charge(amount) => T::Currency::transfer(
origin,
deposit_account,
*amount,
ExistenceRequirement::KeepAlive,
),
// 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.
Expand Down Expand Up @@ -539,8 +513,9 @@ impl<T: Config> Ext<T> for ReservingExt {
panic!("Unable to refund storage deposit. This is a bug.");
}
}
Ok(())
},
};
}
}
}

Expand Down Expand Up @@ -613,7 +588,7 @@ mod tests {
contract: &DepositAccount<Test>,
amount: &DepositOf<Test>,
terminated: bool,
) {
) -> Result<(), DispatchError> {
TestExtTestValue::mutate(|ext| {
ext.charges.push(Charge {
origin: origin.clone(),
Expand All @@ -622,6 +597,7 @@ mod tests {
terminated,
})
});
Ok(())
}
}

Expand Down Expand Up @@ -719,7 +695,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);
Expand Down Expand Up @@ -779,7 +755,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(),
Expand Down
94 changes: 94 additions & 0 deletions frame/contracts/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>},
}
);

Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -2966,6 +2983,83 @@ fn sr25519_verify() {
})
}

#[test]
fn test_failed_charge_should_roll_back_call() {
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
let (wasm_caller, _) = compile_module::<Test>("call_runtime_and_call").unwrap();
let (wasm_callee, _) = compile_module::<Test>("store").unwrap();
pgherveou marked this conversation as resolved.
Show resolved Hide resolved

ExtBuilder::default().existential_deposit(200).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),
vec![],
vec![],
false,
)
.result
.unwrap()
.account_id;
let addr_callee = Contracts::bare_instantiate(
ALICE,
0,
GAS_LIMIT,
None,
Code::Upload(wasm_callee),
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's Alice balance, so we can test a
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
// failed charge.
let min_balance = <Test as Config>::Currency::minimum_balance();
let transfer_call =
Box::new(RuntimeCall::Balances(pallet_balances::Call::transfer_allow_death {
dest: CHARLIE,
value: pallet_balances::Pallet::<Test>::free_balance(&ALICE) - min_balance,
}));

// 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,
});

// bump the deposit per byte to a high value to trigger a FundsUnavailable error.
pgherveou marked this conversation as resolved.
Show resolved Hide resolved
DEPOSIT_PER_BYTE.with(|c| *c.borrow_mut() = 100);

let data = (
100u32, // storage length
addr_callee,
transfer_proxy_call,
);

let result = <Pallet<Test>>::call(
RuntimeOrigin::signed(ALICE),
addr_caller.clone(),
0,
GAS_LIMIT,
None,
data.encode(),
);

pgherveou marked this conversation as resolved.
Show resolved Hide resolved
assert_err_ignore_postinfo!(result, TokenError::FundsUnavailable);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would be nice to test that the transaction gas were actually charge to the user, but I am not too sure how this can be done within our test framework

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can't you just check that Alice's balance became lower than it was before the tx?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated the test, so we can validate that the fees are the same whether the transaction fails (because the payment failed) or not.
I don't think we can test Alice's balance because as far as I know, this would be done in the pre/post hook of the transaction_payment pallet that does not exist in the Test config

for reference

fn pre_dispatch(
self,
who: &Self::AccountId,
call: &Self::Call,
info: &DispatchInfoOf<Self::Call>,
len: usize,
) -> Result<Self::Pre, TransactionValidityError> {
let (_fee, imbalance) = self.withdraw_fee(who, call, info, len)?;
Ok((self.0, who.clone(), imbalance))
}
fn post_dispatch(
maybe_pre: Option<Self::Pre>,
info: &DispatchInfoOf<Self::Call>,
post_info: &PostDispatchInfoOf<Self::Call>,
len: usize,
_result: &DispatchResult,
) -> Result<(), TransactionValidityError> {
if let Some((tip, who, imbalance)) = maybe_pre {
let actual_fee = Pallet::<T>::compute_actual_fee(len as u32, info, post_info, tip);
T::OnChargeTransaction::correct_and_deposit_fee(
&who, info, post_info, actual_fee, tip, imbalance,
)?;
Pallet::<T>::deposit_event(Event::<T>::TransactionFeePaid { who, actual_fee, tip });
}
Ok(())
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can check the events emitted during execution. We do that in other tests. There is a event for transaction fee payment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we would need the transaction_payment pallet for this in test, I don't think we have it 🤔 might have overlooked, will check again

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh yeah. You are right. We didn't set this up in tests. It is fine. We don't really need to test this.

})
}

#[test]
fn upload_code_works() {
let (wasm, code_hash) = compile_module::<Test>("dummy").unwrap();
Expand Down