referrerpolicy=no-referrer-when-downgrade

pallet_revive/evm/
call.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Functionality to decode an eth transaction into an dispatchable call.
19
20use crate::{
21	BalanceOf, CallOf, Config, GenericTransaction, LOG_TARGET, Pallet, RUNTIME_PALLETS_ADDR,
22	Weight, Zero,
23	evm::{
24		TYPE_EIP7702, TYPE_LEGACY,
25		fees::{InfoT, compute_max_integer_quotient},
26		runtime::SetWeightLimit,
27	},
28	extract_code_and_data,
29	metering::EthTxInfo,
30};
31use alloc::{boxed::Box, vec::Vec};
32use codec::DecodeLimit;
33use frame_support::MAX_EXTRINSIC_DEPTH;
34use sp_core::{Get, U256};
35use sp_runtime::{SaturatedConversion, Saturating, transaction_validity::InvalidTransaction};
36
37/// Result of decoding an eth transaction into a dispatchable call.
38pub struct CallInfo<T: Config> {
39	/// The dispatchable call with the correct weights assigned.
40	///
41	/// This will be either `eth_call` or `eth_instantiate_with_code`.
42	pub call: CallOf<T>,
43	/// The weight that was set inside [`Self::call`].
44	pub weight_limit: Weight,
45	/// The encoded length of the bare transaction carrying the ethereum payload.
46	pub encoded_len: u32,
47	/// The adjusted transaction fee of [`Self::call`].
48	pub tx_fee: BalanceOf<T>,
49	/// The additional storage deposit to be deposited into the txhold.
50	pub storage_deposit: BalanceOf<T>,
51	/// The ethereum gas limit of the transaction.
52	pub eth_gas_limit: U256,
53	/// EIP-7702: List of authorization tuples to process
54	pub authorization_list: Vec<crate::evm::AuthorizationListEntry>,
55}
56
57/// Mode for creating a call from an ethereum transaction.
58#[derive(Debug, PartialEq, Eq, Clone)]
59pub enum CreateCallMode {
60	/// Mode for extrinsic execution. Carries the encoding length of the extrinsic and the
61	/// RLP-encoded Ethereum transaction
62	ExtrinsicExecution(u32, Vec<u8>),
63	/// Mode for dry running
64	DryRun,
65}
66
67impl GenericTransaction {
68	/// Decode `tx` into a dispatchable call.
69	pub fn into_call<T>(self, mode: CreateCallMode) -> Result<CallInfo<T>, InvalidTransaction>
70	where
71		T: Config,
72		CallOf<T>: SetWeightLimit,
73	{
74		let is_dry_run = matches!(mode, CreateCallMode::DryRun);
75		let base_fee = <Pallet<T>>::evm_base_fee();
76
77		// We would like to allow for transactions without a chain id to be executed through pallet
78		// revive. These are called unprotected transactions and they are transactions that predate
79		// EIP-155 which do not include a Chain ID. These transactions are still useful today in
80		// certain patterns in Ethereum such as "Nick's Method" for contract deployment which
81		// allows a contract to be deployed on all chains with the same address. This is only
82		// allowed for legacy transactions and isn't allowed for any other transaction type.
83		// * Here's a relevant EIP: https://eips.ethereum.org/EIPS/eip-2470
84		// * Here's Nick's article: https://weka.medium.com/how-to-send-ether-to-11-440-people-187e332566b7
85		match (self.chain_id, self.r#type.as_ref()) {
86			(None, Some(super::Byte(TYPE_LEGACY))) => {},
87			(Some(chain_id), ..) => {
88				if chain_id != <T as Config>::ChainId::get().into() {
89					log::debug!(target: LOG_TARGET, "Invalid chain_id {chain_id:?}");
90					return Err(InvalidTransaction::Call);
91				}
92			},
93			(None, ..) => {
94				log::debug!(target: LOG_TARGET, "Invalid chain_id None");
95				return Err(InvalidTransaction::Call);
96			},
97		}
98
99		let Some(gas) = self.gas else {
100			log::debug!(target: LOG_TARGET, "No gas provided");
101			return Err(InvalidTransaction::Call);
102		};
103
104		// EIP-7702: Validate that type 0x04 transactions have a non-null destination
105		if let Some(super::Byte(TYPE_EIP7702)) = self.r#type.as_ref() {
106			if self.to.is_none() {
107				log::debug!(target: LOG_TARGET, "EIP-7702 transactions require non-null destination");
108				return Err(InvalidTransaction::Call);
109			}
110
111			// EIP-7702: Validate that type 0x04 transactions have non-empty authorization list
112			if self.authorization_list.is_empty() {
113				log::debug!(target: LOG_TARGET, "EIP-7702 transactions require non-empty authorization list");
114				return Err(InvalidTransaction::Call);
115			}
116
117			// EIP-7702 + `RUNTIME_PALLETS_ADDR` is incoherent: that destination
118			// dispatches as `eth_substrate_call`, which has no authorization_list
119			// field, so the auths would be silently dropped.
120			if self.to == Some(RUNTIME_PALLETS_ADDR) {
121				log::debug!(target: LOG_TARGET, "EIP-7702 transactions cannot target RUNTIME_PALLETS_ADDR");
122				return Err(InvalidTransaction::Call);
123			}
124		}
125
126		// EIP-7702: per-tuple field bounds. `chain_id`, `r`, `s` are `U256` (< 2^256 by
127		// construction) and `address` is `H160` (always 20 bytes), so we only need to check
128		// `nonce` and `y_parity` here. Out-of-bounds invalidates the *entire* transaction —
129		// distinct from the per-tuple "nonce fits in u64" (<= 2^64 - 1) check in
130		// `process_authorizations`, which is a processing-step skip.
131		for auth in self.authorization_list.iter() {
132			if auth.nonce.bits() > 64 {
133				log::debug!(
134					target: LOG_TARGET,
135					"EIP-7702 authorization nonce exceeds 2^64: {:?}",
136					auth.nonce,
137				);
138				return Err(InvalidTransaction::Call);
139			}
140			if auth.y_parity.bits() > 8 {
141				log::debug!(
142					target: LOG_TARGET,
143					"EIP-7702 authorization y_parity exceeds 2^8: {:?}",
144					auth.y_parity,
145				);
146				return Err(InvalidTransaction::Call);
147			}
148		}
149
150		// Currently, effective_gas_price will always be the same as base_fee
151		// Because all callers of `into_call` will prepare `tx` that way. Some of the subsequent
152		// logic will not work correctly anymore if we change that assumption.
153		let Some(effective_gas_price) = self.gas_price else {
154			log::debug!(target: LOG_TARGET, "No gas_price provided.");
155			return Err(InvalidTransaction::Payment);
156		};
157
158		if effective_gas_price < base_fee {
159			log::debug!(
160				target: LOG_TARGET,
161				"Specified gas_price is too low. effective_gas_price={effective_gas_price} base_fee={base_fee}"
162			);
163			return Err(InvalidTransaction::Payment);
164		}
165
166		let (encoded_len, transaction_encoded) =
167			if let CreateCallMode::ExtrinsicExecution(encoded_len, transaction_encoded) = mode {
168				(encoded_len, transaction_encoded)
169			} else {
170				// For dry runs, we need to ensure that the RLP encoding length is at least the
171				// length of the encoding of the actual transaction submitted later
172				let mut maximized_tx = self.clone();
173				let maximized_base_fee = base_fee.saturating_mul(256.into());
174				maximized_tx.gas = Some(u64::MAX.into());
175				maximized_tx.gas_price = Some(maximized_base_fee);
176				maximized_tx.max_fee_per_gas = Some(maximized_base_fee);
177				maximized_tx.max_priority_fee_per_gas = Some(maximized_base_fee);
178
179				let unsigned_tx = maximized_tx.try_into_unsigned().map_err(|_| {
180					log::debug!(target: LOG_TARGET, "Invalid transaction type.");
181					InvalidTransaction::Call
182				})?;
183				let transaction_encoded = unsigned_tx.dummy_signed_payload();
184
185				let eth_transact_call =
186					crate::Call::<T>::eth_transact { payload: transaction_encoded.clone() };
187				(<T as Config>::FeeInfo::encoded_len(eth_transact_call.into()), transaction_encoded)
188			};
189
190		let value = self.value.unwrap_or_default();
191		let data = self.input.to_vec();
192
193		let mut call = if let Some(dest) = self.to {
194			if dest == RUNTIME_PALLETS_ADDR {
195				let call =
196					CallOf::<T>::decode_all_with_depth_limit(MAX_EXTRINSIC_DEPTH, &mut &data[..])
197						.map_err(|_| {
198						log::debug!(target: LOG_TARGET, "Failed to decode data as Call");
199						InvalidTransaction::Call
200					})?;
201
202				if !value.is_zero() {
203					log::debug!(target: LOG_TARGET, "Runtime pallets address cannot be called with value");
204					return Err(InvalidTransaction::Call);
205				}
206
207				crate::Call::eth_substrate_call::<T> { call: Box::new(call), transaction_encoded }
208					.into()
209			} else {
210				crate::Call::eth_call::<T> {
211					dest,
212					value,
213					weight_limit: Zero::zero(),
214					eth_gas_limit: gas,
215					data,
216					transaction_encoded,
217					effective_gas_price,
218					encoded_len,
219					authorization_list: self.authorization_list.clone(),
220				}
221				.into()
222			}
223		} else {
224			let (code, data) = if data.starts_with(&polkavm_common::program::BLOB_MAGIC) {
225				let Some((code, data)) = extract_code_and_data(&data) else {
226					log::debug!(target: LOG_TARGET, "Failed to extract polkavm code & data");
227					return Err(InvalidTransaction::Call);
228				};
229				(code, data)
230			} else {
231				(data, Default::default())
232			};
233
234			let call = crate::Call::eth_instantiate_with_code::<T> {
235				value,
236				weight_limit: Zero::zero(),
237				eth_gas_limit: gas,
238				code,
239				data,
240				transaction_encoded,
241				effective_gas_price,
242				encoded_len,
243			}
244			.into();
245
246			call
247		};
248
249		// the fee as signed off by the eth wallet. we cannot consume more.
250		let eth_fee =
251			effective_gas_price.saturating_mul(gas) / <T as Config>::NativeToEthRatio::get();
252
253		let weight_limit = {
254			let fixed_fee = <T as Config>::FeeInfo::fixed_fee(encoded_len as u32);
255			let info = <T as Config>::FeeInfo::dispatch_info(&call);
256
257			let remaining_fee = {
258				let adjusted = eth_fee.checked_sub(fixed_fee.into()).ok_or_else(|| {
259				log::debug!(target: LOG_TARGET, "Not enough gas supplied to cover base and len fee. eth_fee={eth_fee:?} fixed_fee={fixed_fee:?}");
260				InvalidTransaction::Payment
261			})?;
262
263				let unadjusted = compute_max_integer_quotient(
264					<T as Config>::FeeInfo::next_fee_multiplier(),
265					<BalanceOf<T>>::saturated_from(adjusted),
266				);
267
268				unadjusted
269			};
270			let remaining_fee_weight = <T as Config>::FeeInfo::fee_to_weight(remaining_fee);
271			let weight_limit = remaining_fee_weight
272			.checked_sub(&info.total_weight()).ok_or_else(|| {
273			log::debug!(target: LOG_TARGET, "Not enough gas supplied to cover the weight ({:?}) of the extrinsic. remaining_fee_weight: {remaining_fee_weight:?}", info.total_weight(),);
274			InvalidTransaction::Payment
275		})?;
276
277			call.set_weight_limit(weight_limit);
278
279			if !is_dry_run {
280				let max_weight = <Pallet<T>>::evm_max_extrinsic_weight();
281				let info = <T as Config>::FeeInfo::dispatch_info(&call);
282				let overweight_by = info.total_weight().saturating_sub(max_weight);
283				let capped_weight = weight_limit.saturating_sub(overweight_by);
284				call.set_weight_limit(capped_weight);
285				capped_weight
286			} else {
287				weight_limit
288			}
289		};
290
291		// the overall fee of the extrinsic including the gas limit
292		let tx_fee = <T as Config>::FeeInfo::tx_fee(encoded_len, &call);
293
294		// the leftover we make available to the deposit collection system
295		let storage_deposit = eth_fee.checked_sub(tx_fee.into()).ok_or_else(|| {
296			log::error!(target: LOG_TARGET, "The eth_fee={eth_fee:?} is smaller than the tx_fee={tx_fee:?}. This is a bug.");
297			InvalidTransaction::Payment
298		})?.saturated_into();
299
300		// EIP-7702: Ensure enough gas to cover worst-case deposits for authorizations
301		// (ED + contract storage deposit + code lockup per authorization).
302		if !self.authorization_list.is_empty() {
303			let info = <T as Config>::FeeInfo::base_dispatch_info(&mut call);
304			let max_deposit = EthTxInfo::<T>::new(encoded_len, info.total_weight())
305				.max_deposit(gas.saturated_into());
306
307			let auth_cost = <Pallet<T>>::worst_case_delegation_deposit()
308				.saturating_mul(self.authorization_list.len().saturated_into());
309
310			if max_deposit < auth_cost {
311				log::debug!(
312					target: LOG_TARGET,
313					"Not enough gas to cover deposits for authorization accounts. \
314					max_deposit={max_deposit:?} required={auth_cost:?}"
315				);
316				return Err(InvalidTransaction::Payment);
317			}
318		}
319
320		Ok(CallInfo {
321			call,
322			weight_limit,
323			encoded_len,
324			tx_fee,
325			storage_deposit,
326			eth_gas_limit: gas,
327			authorization_list: self.authorization_list,
328		})
329	}
330}