From f85d6dc6dda6e96fb1ce40ad42fdc3263c23b906 Mon Sep 17 00:00:00 2001
From: Sasha Gryaznov <hi@agryaznov.com>
Date: Mon, 6 Mar 2023 10:40:03 +0200
Subject: [PATCH] [contracts] Forbid calling back to contracts after switching
 to runtime (#13443)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* save: compiles and tests pass

* save: added global

* done + test

* cleanup

* changelog update

* cleanup

* address feedback, step 1

* address feedback, step 2

* address feedback, step 3

* returned updated gas_estimation_call_runtime test

* clippy fix

* address feedback, step 4

* address feedback, step 5

* move data from context to inputs

* docs fix

* Apply suggestions from code review

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* address feedback, step 6

---------

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
---
 substrate/Cargo.lock                   |   1 +
 substrate/frame/contracts/CHANGELOG.md |   3 +
 substrate/frame/contracts/Cargo.toml   |   2 +
 substrate/frame/contracts/src/lib.rs   | 359 +++++++++++++++----------
 substrate/frame/contracts/src/tests.rs |  72 ++++-
 5 files changed, 287 insertions(+), 150 deletions(-)

diff --git a/substrate/Cargo.lock b/substrate/Cargo.lock
index d67aeda9f03..5a215553d15 100644
--- a/substrate/Cargo.lock
+++ b/substrate/Cargo.lock
@@ -5598,6 +5598,7 @@ dependencies = [
  "assert_matches",
  "bitflags",
  "env_logger 0.9.3",
+ "environmental",
  "frame-benchmarking",
  "frame-support",
  "frame-system",
diff --git a/substrate/frame/contracts/CHANGELOG.md b/substrate/frame/contracts/CHANGELOG.md
index ab3998e6dc4..dcb9d6d4d2b 100644
--- a/substrate/frame/contracts/CHANGELOG.md
+++ b/substrate/frame/contracts/CHANGELOG.md
@@ -20,6 +20,9 @@ In other words: Upgrading this pallet will not break pre-existing contracts.
 
 ### Added
 
+- Forbid calling back to contracts after switching to runtime
+[#13443](https://github.com/paritytech/substrate/pull/13443)
+
 - Allow contracts to dispatch calls into the runtime (**unstable**)
 [#9276](https://github.com/paritytech/substrate/pull/9276)
 
diff --git a/substrate/frame/contracts/Cargo.toml b/substrate/frame/contracts/Cargo.toml
index cd41ae2e39b..e19326b785b 100644
--- a/substrate/frame/contracts/Cargo.toml
+++ b/substrate/frame/contracts/Cargo.toml
@@ -35,6 +35,7 @@ rand = { version = "0.8", optional = true, default-features = false }
 rand_pcg = { version = "0.3", optional = true }
 
 # Substrate Dependencies
+environmental = { version = "1.1.4", default-features = false }
 frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true }
 frame-support = { version = "4.0.0-dev", default-features = false, path = "../support" }
 frame-system = { version = "4.0.0-dev", default-features = false, path = "../system" }
@@ -80,6 +81,7 @@ std = [
 	"log/std",
 	"rand/std",
 	"wasmparser/std",
+	"environmental/std",
 ]
 runtime-benchmarks = [
 	"frame-benchmarking/runtime-benchmarks",
diff --git a/substrate/frame/contracts/src/lib.rs b/substrate/frame/contracts/src/lib.rs
index a115cf119ce..4f5d7ba305f 100644
--- a/substrate/frame/contracts/src/lib.rs
+++ b/substrate/frame/contracts/src/lib.rs
@@ -100,13 +100,14 @@ pub mod weights;
 mod tests;
 
 use crate::{
-	exec::{AccountIdOf, ExecError, Executable, Stack as ExecStack},
+	exec::{AccountIdOf, ErrorOrigin, ExecError, Executable, Stack as ExecStack},
 	gas::GasMeter,
 	storage::{meter::Meter as StorageMeter, ContractInfo, DeletedContract},
 	wasm::{OwnerInfo, PrefabWasmModule, TryInstantiate},
 	weights::WeightInfo,
 };
 use codec::{Codec, Encode, HasCompact};
+use environmental::*;
 use frame_support::{
 	dispatch::{Dispatchable, GetDispatchInfo, Pays, PostDispatchInfo},
 	ensure,
@@ -616,16 +617,16 @@ pub mod pallet {
 			let gas_limit: Weight = gas_limit.into();
 			let origin = ensure_signed(origin)?;
 			let dest = T::Lookup::lookup(dest)?;
-			let mut output = Self::internal_call(
+			let common = CommonInput {
 				origin,
-				dest,
 				value,
-				gas_limit,
-				storage_deposit_limit.map(Into::into),
 				data,
-				None,
-				Determinism::Deterministic,
-			);
+				gas_limit,
+				storage_deposit_limit: storage_deposit_limit.map(Into::into),
+				debug_message: None,
+			};
+			let mut output = CallInput::<T> { dest, determinism: Determinism::Deterministic }
+				.run_guarded(common);
 			if let Ok(retval) = &output.result {
 				if retval.did_revert() {
 					output.result = Err(<Error<T>>::ContractReverted.into());
@@ -678,16 +679,16 @@ pub mod pallet {
 			let code_len = code.len() as u32;
 			let data_len = data.len() as u32;
 			let salt_len = salt.len() as u32;
-			let mut output = Self::internal_instantiate(
+			let common = CommonInput {
 				origin,
 				value,
-				gas_limit,
-				storage_deposit_limit.map(Into::into),
-				Code::Upload(code),
 				data,
-				salt,
-				None,
-			);
+				gas_limit,
+				storage_deposit_limit: storage_deposit_limit.map(Into::into),
+				debug_message: None,
+			};
+			let mut output =
+				InstantiateInput::<T> { code: Code::Upload(code), salt }.run_guarded(common);
 			if let Ok(retval) = &output.result {
 				if retval.1.did_revert() {
 					output.result = Err(<Error<T>>::ContractReverted.into());
@@ -720,16 +721,16 @@ pub mod pallet {
 			let origin = ensure_signed(origin)?;
 			let data_len = data.len() as u32;
 			let salt_len = salt.len() as u32;
-			let mut output = Self::internal_instantiate(
+			let common = CommonInput {
 				origin,
 				value,
-				gas_limit,
-				storage_deposit_limit.map(Into::into),
-				Code::Existing(code_hash),
 				data,
-				salt,
-				None,
-			);
+				gas_limit,
+				storage_deposit_limit: storage_deposit_limit.map(Into::into),
+				debug_message: None,
+			};
+			let mut output =
+				InstantiateInput::<T> { code: Code::Existing(code_hash), salt }.run_guarded(common);
 			if let Ok(retval) = &output.result {
 				if retval.1.did_revert() {
 					output.result = Err(<Error<T>>::ContractReverted.into());
@@ -872,6 +873,9 @@ pub mod pallet {
 		/// This can be triggered by a call to `seal_terminate`.
 		TerminatedInConstructor,
 		/// A call tried to invoke a contract that is flagged as non-reentrant.
+		/// The only other cause is that a call from a contract into the runtime tried to call back
+		/// into `pallet-contracts`. This would make the whole pallet reentrant with regard to
+		/// contract code execution which is not supported.
 		ReentranceDenied,
 		/// Origin doesn't have enough balance to pay the required storage deposits.
 		StorageDepositNotEnoughFunds,
@@ -951,11 +955,27 @@ pub mod pallet {
 		StorageValue<_, BoundedVec<DeletedContract, T::DeletionQueueDepth>, ValueQuery>;
 }
 
-/// Return type of the private [`Pallet::internal_call`] function.
-type InternalCallOutput<T> = InternalOutput<T, ExecReturnValue>;
+/// Context of a contract invocation.
+struct CommonInput<'a, T: Config> {
+	origin: T::AccountId,
+	value: BalanceOf<T>,
+	data: Vec<u8>,
+	gas_limit: Weight,
+	storage_deposit_limit: Option<BalanceOf<T>>,
+	debug_message: Option<&'a mut DebugBufferVec<T>>,
+}
 
-/// Return type of the private [`Pallet::internal_instantiate`] function.
-type InternalInstantiateOutput<T> = InternalOutput<T, (AccountIdOf<T>, ExecReturnValue)>;
+/// Input specific to a call into contract.
+struct CallInput<T: Config> {
+	dest: T::AccountId,
+	determinism: Determinism,
+}
+
+/// Input specific to a contract instantiation invocation.
+struct InstantiateInput<T: Config> {
+	code: Code<CodeHash<T>>,
+	salt: Vec<u8>,
+}
 
 /// Return type of private helper functions.
 struct InternalOutput<T: Config, O> {
@@ -967,6 +987,164 @@ struct InternalOutput<T: Config, O> {
 	result: Result<O, ExecError>,
 }
 
+/// Helper trait to wrap contract execution entry points into a signle function
+/// [`Invokable::run_guarded`].
+trait Invokable<T: Config> {
+	/// What is returned as a result of a successful invocation.
+	type Output;
+
+	/// Single entry point to contract execution.
+	/// Downstream execution flow is branched by implementations of [`Invokable`] trait:
+	///
+	/// - [`InstantiateInput::run`] runs contract instantiation,
+	/// - [`CallInput::run`] runs contract call.
+	///
+	/// We enforce a re-entrancy guard here by initializing and checking a boolean flag through a
+	/// global reference.
+	fn run_guarded(&self, common: CommonInput<T>) -> InternalOutput<T, Self::Output> {
+		// Set up a global reference to the boolean flag used for the re-entrancy guard.
+		environmental!(executing_contract: bool);
+
+		let gas_limit = common.gas_limit;
+		executing_contract::using_once(&mut false, || {
+			executing_contract::with(|f| {
+				// Fail if already entered contract execution
+				if *f {
+					return Err(())
+				}
+				// We are entering contract execution
+				*f = true;
+				Ok(())
+			})
+			.expect("Returns `Ok` if called within `using_once`. It is syntactically obvious that this is the case; qed")
+			.map_or_else(
+				|_| InternalOutput {
+					gas_meter: GasMeter::new(gas_limit),
+					storage_deposit: Default::default(),
+					result: Err(ExecError {
+						error: <Error<T>>::ReentranceDenied.into(),
+						origin: ErrorOrigin::Caller,
+					}),
+				},
+				// Enter contract call.
+				|_| self.run(common, GasMeter::new(gas_limit)),
+			)
+		})
+	}
+
+	/// Method that does the actual call to a contract. It can be either a call to a deployed
+	/// contract or a instantiation of a new one.
+	///
+	/// Called by dispatchables and public functions through the [`Invokable::run_guarded`].
+	fn run(
+		&self,
+		common: CommonInput<T>,
+		gas_meter: GasMeter<T>,
+	) -> InternalOutput<T, Self::Output>;
+}
+
+impl<T: Config> Invokable<T> for CallInput<T> {
+	type Output = ExecReturnValue;
+
+	fn run(
+		&self,
+		common: CommonInput<T>,
+		mut gas_meter: GasMeter<T>,
+	) -> InternalOutput<T, Self::Output> {
+		let mut storage_meter =
+			match StorageMeter::new(&common.origin, common.storage_deposit_limit, common.value) {
+				Ok(meter) => meter,
+				Err(err) =>
+					return InternalOutput {
+						result: Err(err.into()),
+						gas_meter,
+						storage_deposit: Default::default(),
+					},
+			};
+		let schedule = T::Schedule::get();
+		let CallInput { dest, determinism } = self;
+		let CommonInput { origin, value, data, debug_message, .. } = common;
+		let result = ExecStack::<T, PrefabWasmModule<T>>::run_call(
+			origin.clone(),
+			dest.clone(),
+			&mut gas_meter,
+			&mut storage_meter,
+			&schedule,
+			value,
+			data.clone(),
+			debug_message,
+			*determinism,
+		);
+		InternalOutput { gas_meter, storage_deposit: storage_meter.into_deposit(&origin), result }
+	}
+}
+
+impl<T: Config> Invokable<T> for InstantiateInput<T> {
+	type Output = (AccountIdOf<T>, ExecReturnValue);
+
+	fn run(
+		&self,
+		mut common: CommonInput<T>,
+		mut gas_meter: GasMeter<T>,
+	) -> InternalOutput<T, Self::Output> {
+		let mut storage_deposit = Default::default();
+		let try_exec = || {
+			let schedule = T::Schedule::get();
+			let (extra_deposit, executable) = match &self.code {
+				Code::Upload(binary) => {
+					let executable = PrefabWasmModule::from_code(
+						binary.clone(),
+						&schedule,
+						common.origin.clone(),
+						Determinism::Deterministic,
+						TryInstantiate::Skip,
+					)
+					.map_err(|(err, msg)| {
+						common
+							.debug_message
+							.as_mut()
+							.map(|buffer| buffer.try_extend(&mut msg.bytes()));
+						err
+					})?;
+					// The open deposit will be charged during execution when the
+					// uploaded module does not already exist. This deposit is not part of the
+					// storage meter because it is not transferred to the contract but
+					// reserved on the uploading account.
+					(executable.open_deposit(), executable)
+				},
+				Code::Existing(hash) => (
+					Default::default(),
+					PrefabWasmModule::from_storage(*hash, &schedule, &mut gas_meter)?,
+				),
+			};
+			let mut storage_meter = StorageMeter::new(
+				&common.origin,
+				common.storage_deposit_limit,
+				common.value.saturating_add(extra_deposit),
+			)?;
+
+			let InstantiateInput { salt, .. } = self;
+			let CommonInput { origin, value, data, debug_message, .. } = common;
+			let result = ExecStack::<T, PrefabWasmModule<T>>::run_instantiate(
+				origin.clone(),
+				executable,
+				&mut gas_meter,
+				&mut storage_meter,
+				&schedule,
+				value,
+				data.clone(),
+				&salt,
+				debug_message,
+			);
+			storage_deposit = storage_meter
+				.into_deposit(&origin)
+				.saturating_add(&StorageDeposit::Charge(extra_deposit));
+			result
+		};
+		InternalOutput { result: try_exec(), gas_meter, storage_deposit }
+	}
+}
+
 impl<T: Config> Pallet<T> {
 	/// Perform a call to a specified contract.
 	///
@@ -991,16 +1169,15 @@ impl<T: Config> Pallet<T> {
 		determinism: Determinism,
 	) -> ContractExecResult<BalanceOf<T>> {
 		let mut debug_message = if debug { Some(DebugBufferVec::<T>::default()) } else { None };
-		let output = Self::internal_call(
+		let common = CommonInput {
 			origin,
-			dest,
 			value,
+			data,
 			gas_limit,
 			storage_deposit_limit,
-			data,
-			debug_message.as_mut(),
-			determinism,
-		);
+			debug_message: debug_message.as_mut(),
+		};
+		let output = CallInput::<T> { dest, determinism }.run_guarded(common);
 		ContractExecResult {
 			result: output.result.map_err(|r| r.error),
 			gas_consumed: output.gas_meter.gas_consumed(),
@@ -1033,16 +1210,15 @@ impl<T: Config> Pallet<T> {
 		debug: bool,
 	) -> ContractInstantiateResult<T::AccountId, BalanceOf<T>> {
 		let mut debug_message = if debug { Some(DebugBufferVec::<T>::default()) } else { None };
-		let output = Self::internal_instantiate(
+		let common = CommonInput {
 			origin,
 			value,
+			data,
 			gas_limit,
 			storage_deposit_limit,
-			code,
-			data,
-			salt,
-			debug_message.as_mut(),
-		);
+			debug_message: debug_message.as_mut(),
+		};
+		let output = InstantiateInput::<T> { code, salt }.run_guarded(common);
 		ContractInstantiateResult {
 			result: output
 				.result
@@ -1132,113 +1308,6 @@ impl<T: Config> Pallet<T> {
 		self::wasm::reinstrument(module, schedule).map(|_| ())
 	}
 
-	/// Internal function that does the actual call.
-	///
-	/// Called by dispatchables and public functions.
-	fn internal_call(
-		origin: T::AccountId,
-		dest: T::AccountId,
-		value: BalanceOf<T>,
-		gas_limit: Weight,
-		storage_deposit_limit: Option<BalanceOf<T>>,
-		data: Vec<u8>,
-		debug_message: Option<&mut DebugBufferVec<T>>,
-		determinism: Determinism,
-	) -> InternalCallOutput<T> {
-		let mut gas_meter = GasMeter::new(gas_limit);
-		let mut storage_meter = match StorageMeter::new(&origin, storage_deposit_limit, value) {
-			Ok(meter) => meter,
-			Err(err) =>
-				return InternalCallOutput {
-					result: Err(err.into()),
-					gas_meter,
-					storage_deposit: Default::default(),
-				},
-		};
-		let schedule = T::Schedule::get();
-		let result = ExecStack::<T, PrefabWasmModule<T>>::run_call(
-			origin.clone(),
-			dest,
-			&mut gas_meter,
-			&mut storage_meter,
-			&schedule,
-			value,
-			data,
-			debug_message,
-			determinism,
-		);
-		InternalCallOutput {
-			result,
-			gas_meter,
-			storage_deposit: storage_meter.into_deposit(&origin),
-		}
-	}
-
-	/// Internal function that does the actual instantiation.
-	///
-	/// Called by dispatchables and public functions.
-	fn internal_instantiate(
-		origin: T::AccountId,
-		value: BalanceOf<T>,
-		gas_limit: Weight,
-		storage_deposit_limit: Option<BalanceOf<T>>,
-		code: Code<CodeHash<T>>,
-		data: Vec<u8>,
-		salt: Vec<u8>,
-		mut debug_message: Option<&mut DebugBufferVec<T>>,
-	) -> InternalInstantiateOutput<T> {
-		let mut storage_deposit = Default::default();
-		let mut gas_meter = GasMeter::new(gas_limit);
-		let try_exec = || {
-			let schedule = T::Schedule::get();
-			let (extra_deposit, executable) = match code {
-				Code::Upload(binary) => {
-					let executable = PrefabWasmModule::from_code(
-						binary,
-						&schedule,
-						origin.clone(),
-						Determinism::Deterministic,
-						TryInstantiate::Skip,
-					)
-					.map_err(|(err, msg)| {
-						debug_message.as_mut().map(|buffer| buffer.try_extend(&mut msg.bytes()));
-						err
-					})?;
-					// The open deposit will be charged during execution when the
-					// uploaded module does not already exist. This deposit is not part of the
-					// storage meter because it is not transferred to the contract but
-					// reserved on the uploading account.
-					(executable.open_deposit(), executable)
-				},
-				Code::Existing(hash) => (
-					Default::default(),
-					PrefabWasmModule::from_storage(hash, &schedule, &mut gas_meter)?,
-				),
-			};
-			let mut storage_meter = StorageMeter::new(
-				&origin,
-				storage_deposit_limit,
-				value.saturating_add(extra_deposit),
-			)?;
-			let result = ExecStack::<T, PrefabWasmModule<T>>::run_instantiate(
-				origin.clone(),
-				executable,
-				&mut gas_meter,
-				&mut storage_meter,
-				&schedule,
-				value,
-				data,
-				&salt,
-				debug_message,
-			);
-			storage_deposit = storage_meter
-				.into_deposit(&origin)
-				.saturating_add(&StorageDeposit::Charge(extra_deposit));
-			result
-		};
-		InternalInstantiateOutput { result: try_exec(), gas_meter, storage_deposit }
-	}
-
 	/// Deposit a pallet contracts event. Handles the conversion to the overarching event type.
 	fn deposit_event(topics: Vec<T::Hash>, event: Event<T>) {
 		<frame_system::Pallet<T>>::deposit_event_indexed(
diff --git a/substrate/frame/contracts/src/tests.rs b/substrate/frame/contracts/src/tests.rs
index cecac03388d..d74b08243df 100644
--- a/substrate/frame/contracts/src/tests.rs
+++ b/substrate/frame/contracts/src/tests.rs
@@ -2805,12 +2805,9 @@ fn gas_estimation_call_runtime() {
 
 		// Call something trivial with a huge gas limit so that we can observe the effects
 		// of pre-charging. This should create a difference between consumed and required.
-		let call = RuntimeCall::Contracts(crate::Call::call {
+		let call = RuntimeCall::Balances(pallet_balances::Call::transfer {
 			dest: addr_callee,
-			value: 0,
-			gas_limit: GAS_LIMIT / 3,
-			storage_deposit_limit: None,
-			data: vec![],
+			value: min_balance * 10,
 		});
 		let result = Contracts::bare_call(
 			ALICE,
@@ -2844,6 +2841,71 @@ fn gas_estimation_call_runtime() {
 	});
 }
 
+#[test]
+fn gas_call_runtime_reentrancy_guarded() {
+	let (caller_code, _caller_hash) = compile_module::<Test>("call_runtime").unwrap();
+	let (callee_code, _callee_hash) = compile_module::<Test>("dummy").unwrap();
+	ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
+		let min_balance = <Test as Config>::Currency::minimum_balance();
+		let _ = Balances::deposit_creating(&ALICE, 1000 * min_balance);
+		let _ = Balances::deposit_creating(&CHARLIE, 1000 * min_balance);
+
+		let addr_caller = Contracts::bare_instantiate(
+			ALICE,
+			min_balance * 100,
+			GAS_LIMIT,
+			None,
+			Code::Upload(caller_code),
+			vec![],
+			vec![0],
+			false,
+		)
+		.result
+		.unwrap()
+		.account_id;
+
+		let addr_callee = Contracts::bare_instantiate(
+			ALICE,
+			min_balance * 100,
+			GAS_LIMIT,
+			None,
+			Code::Upload(callee_code),
+			vec![],
+			vec![1],
+			false,
+		)
+		.result
+		.unwrap()
+		.account_id;
+
+		// Call pallet_contracts call() dispatchable
+		let call = RuntimeCall::Contracts(crate::Call::call {
+			dest: addr_callee,
+			value: 0,
+			gas_limit: GAS_LIMIT / 3,
+			storage_deposit_limit: None,
+			data: vec![],
+		});
+
+		// Call runtime to re-enter back to contracts engine by
+		// calling dummy contract
+		let result = Contracts::bare_call(
+			ALICE,
+			addr_caller.clone(),
+			0,
+			GAS_LIMIT,
+			None,
+			call.encode(),
+			false,
+			Determinism::Deterministic,
+		)
+		.result
+		.unwrap();
+		// Call to runtime should fail because of the re-entrancy guard
+		assert_return_code!(result, RuntimeReturnCode::CallRuntimeFailed);
+	});
+}
+
 #[test]
 fn ecdsa_recover() {
 	let (wasm, _code_hash) = compile_module::<Test>("ecdsa_recover").unwrap();
-- 
GitLab