pallet_ethereum_checked/
lib.rs

1// This file is part of Astar.
2
3// Copyright (C) Stake Technologies Pte.Ltd.
4// SPDX-License-Identifier: GPL-3.0-or-later
5
6// Astar is free software: you can redistribute it and/or modify
7// it under the terms of the GNU General Public License as published by
8// the Free Software Foundation, either version 3 of the License, or
9// (at your option) any later version.
10
11// Astar is distributed in the hope that it will be useful,
12// but WITHOUT ANY WARRANTY; without even the implied warranty of
13// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14// GNU General Public License for more details.
15
16// You should have received a copy of the GNU General Public License
17// along with Astar. If not, see <http://www.gnu.org/licenses/>.
18
19//! # Ethereum Checked Pallet
20//!
21//! ## Overview
22//!
23//! A `pallet-ethereum` like pallet that execute transactions from checked source,
24//! like XCM remote call. Only `Call` transactions are supported
25//! (no `Create`).
26//!
27//! The checked source guarantees that transactions are valid with prior checks, so these
28//! transactions are not required to include valid signatures. Instead, `pallet-ethereum-checked`
29//! will add the same dummy signature to them. To avoid transaction hash collisions, a global
30//! nonce shared with all users are used.
31//!
32//! ## Interface
33//!
34//! ### Dispatch-able calls
35//!
36//! - `transact`: transact an Ethereum transaction. Similar to `pallet_ethereum::Transact`,
37//! but is only for XCM remote call.
38//!
39
40#![cfg_attr(not(feature = "std"), no_std)]
41
42use parity_scale_codec::{Decode, Encode};
43use scale_info::TypeInfo;
44
45use ethereum_types::U256;
46use fp_ethereum::{Transaction, TransactionData, ValidatedTransaction};
47use fp_evm::{
48    CallInfo, CallOrCreateInfo, CheckEvmTransaction, CheckEvmTransactionConfig, ExitReason,
49    ExitSucceed, TransactionValidationError,
50};
51use pallet_evm::GasWeightMapping;
52
53use frame_support::{
54    dispatch::{DispatchErrorWithPostInfo, PostDispatchInfo},
55    pallet_prelude::*,
56};
57use frame_system::pallet_prelude::*;
58#[cfg(feature = "runtime-benchmarks")]
59use sp_runtime::traits::TrailingZeroInput;
60use sp_runtime::traits::UniqueSaturatedInto;
61use sp_std::{marker::PhantomData, result::Result};
62
63use astar_primitives::{
64    ethereum_checked::CheckedEthereumTx,
65    evm::{UnifiedAddressMapper, H160},
66};
67
68pub use pallet::*;
69
70#[cfg(feature = "runtime-benchmarks")]
71mod benchmarking;
72
73// TODO: after integrated into Astar/Shiden runtime, redo benchmarking with them.
74// The reason is that `EVMChainId` storage read only happens in Shibuya
75pub mod weights;
76pub use weights::WeightInfo;
77
78mod mock;
79mod tests;
80
81pub type WeightInfoOf<T> = <T as Config>::WeightInfo;
82
83/// Origin for dispatch-able calls.
84#[derive(
85    PartialEq,
86    Eq,
87    Clone,
88    Encode,
89    Decode,
90    DecodeWithMemTracking,
91    RuntimeDebug,
92    TypeInfo,
93    MaxEncodedLen,
94)]
95pub enum RawOrigin<AccountId> {
96    XcmEthereumTx(AccountId),
97}
98
99/// Ensure the origin is with XCM calls.
100pub struct EnsureXcmEthereumTx<AccountId>(PhantomData<AccountId>);
101impl<O: Into<Result<RawOrigin<AccountId>, O>> + From<RawOrigin<AccountId>>, AccountId: Decode>
102    EnsureOrigin<O> for EnsureXcmEthereumTx<AccountId>
103{
104    type Success = AccountId;
105
106    fn try_origin(o: O) -> Result<Self::Success, O> {
107        o.into().map(|o| match o {
108            RawOrigin::XcmEthereumTx(account_id) => account_id,
109        })
110    }
111
112    #[cfg(feature = "runtime-benchmarks")]
113    fn try_successful_origin() -> Result<O, ()> {
114        let zero_account_id =
115            AccountId::decode(&mut TrailingZeroInput::zeroes()).map_err(|_| ())?;
116        Ok(O::from(RawOrigin::XcmEthereumTx(zero_account_id)))
117    }
118}
119
120/// Transaction kind.
121#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)]
122pub enum CheckedEthereumTxKind {
123    /// The tx is from XCM remote call.
124    Xcm,
125}
126
127#[frame_support::pallet]
128pub mod pallet {
129    use super::*;
130
131    #[pallet::pallet]
132    pub struct Pallet<T>(PhantomData<T>);
133
134    #[pallet::config]
135    pub trait Config: frame_system::Config + pallet_evm::Config {
136        /// Reserved Xcmp weight for block gas limit calculation.
137        type ReservedXcmpWeight: Get<Weight>;
138
139        /// Invalid tx error.
140        type InvalidEvmTransactionError: From<TransactionValidationError>;
141
142        /// Validated tx execution.
143        type ValidatedTransaction: ValidatedTransaction;
144
145        /// Account mapping.
146        type AddressMapper: UnifiedAddressMapper<Self::AccountId>;
147
148        /// Origin for `transact` call.
149        type XcmTransactOrigin: EnsureOrigin<Self::RuntimeOrigin, Success = Self::AccountId>;
150
151        /// Weight information for extrinsics in this pallet.
152        type WeightInfo: WeightInfo;
153    }
154
155    #[pallet::origin]
156    pub type Origin<T> = RawOrigin<<T as frame_system::Config>::AccountId>;
157
158    /// Global nonce for all transactions to avoid hash collision, which is
159    /// caused by the same dummy signatures for all transactions.
160    #[pallet::storage]
161    pub type Nonce<T: Config> = StorageValue<_, U256, ValueQuery>;
162
163    #[pallet::call]
164    impl<T: Config> Pallet<T> {
165        /// Transact an Ethereum transaction. Similar to `pallet_ethereum::Transact`,
166        /// but is only for XCM remote call.
167        #[pallet::call_index(0)]
168        #[pallet::weight({
169            let weight_limit = T::GasWeightMapping::gas_to_weight(tx.gas_limit.unique_saturated_into(), false);
170            weight_limit.saturating_add(WeightInfoOf::<T>::transact_without_apply())
171        })]
172        pub fn transact(origin: OriginFor<T>, tx: CheckedEthereumTx) -> DispatchResultWithPostInfo {
173            let source = T::XcmTransactOrigin::ensure_origin(origin)?;
174            Self::do_transact(
175                T::AddressMapper::to_h160_or_default(&source).into_address(),
176                tx.into(),
177                CheckedEthereumTxKind::Xcm,
178                false,
179            )
180            .map(|(post_info, _)| post_info)
181        }
182    }
183}
184
185impl<T: Config> Pallet<T> {
186    /// Validate and execute the checked tx. Only `Call` transaction action is allowed.
187    fn do_transact(
188        source: H160,
189        checked_tx: CheckedEthereumTx,
190        tx_kind: CheckedEthereumTxKind,
191        skip_apply: bool,
192    ) -> Result<(PostDispatchInfo, CallInfo), DispatchErrorWithPostInfo> {
193        let chain_id = T::ChainId::get();
194        let nonce = Nonce::<T>::get();
195        let tx: Transaction = checked_tx
196            .into_ethereum_tx(Nonce::<T>::get(), chain_id)
197            .into();
198        let tx_data: TransactionData = (&tx).into();
199
200        let (weight_limit, proof_size_base_cost) =
201            match <T as pallet_evm::Config>::GasWeightMapping::gas_to_weight(
202                tx_data.gas_limit.unique_saturated_into(),
203                true,
204            ) {
205                weight_limit if weight_limit.proof_size() > 0 => (
206                    Some(weight_limit),
207                    // measured PoV should be correct to use here
208                    Some(WeightInfoOf::<T>::transact_without_apply().proof_size()),
209                ),
210                _ => (None, None),
211            };
212
213        // Validate the tx.
214        let _ = CheckEvmTransaction::<T::InvalidEvmTransactionError>::new(
215            CheckEvmTransactionConfig {
216                evm_config: T::config(),
217                block_gas_limit: U256::from(Self::block_gas_limit(&tx_kind)),
218                base_fee: U256::zero(),
219                chain_id,
220                is_transactional: true,
221            },
222            tx_data.into(),
223            weight_limit,
224            proof_size_base_cost,
225        )
226        // Gas limit validation. The fee payment has been validated as the tx is `checked`.
227        .validate_common()
228        .map_err(|_| DispatchErrorWithPostInfo {
229            post_info: PostDispatchInfo {
230                // actual_weight = overhead - nonce_write_1
231                actual_weight: Some(
232                    WeightInfoOf::<T>::transact_without_apply()
233                        .saturating_sub(T::DbWeight::get().writes(1)),
234                ),
235                pays_fee: Pays::Yes,
236            },
237            error: DispatchError::Other("Failed to validate Ethereum tx"),
238        })?;
239
240        Nonce::<T>::put(nonce.saturating_add(U256::one()));
241
242        if skip_apply {
243            return Ok((
244                PostDispatchInfo {
245                    actual_weight: Some(WeightInfoOf::<T>::transact_without_apply()),
246                    pays_fee: Pays::Yes,
247                },
248                CallInfo {
249                    exit_reason: ExitReason::Succeed(ExitSucceed::Returned),
250                    value: Default::default(),
251                    used_gas: fp_evm::UsedGas {
252                        standard: checked_tx.gas_limit,
253                        effective: checked_tx.gas_limit,
254                    },
255                    weight_info: None,
256                    logs: Default::default(),
257                },
258            ));
259        }
260
261        // Execute the tx.
262        let (post_info, apply_info) = T::ValidatedTransaction::apply(source, tx, None)?;
263        match apply_info {
264            CallOrCreateInfo::Call(info) => Ok((post_info, info)),
265            // It is not possible to have a `Create` transaction via `CheckedEthereumTx`.
266            CallOrCreateInfo::Create(_) => {
267                unreachable!("Cannot create a 'Create' transaction; qed")
268            }
269        }
270    }
271
272    /// Block gas limit calculation based on the tx kind.
273    fn block_gas_limit(tx_kind: &CheckedEthereumTxKind) -> u64 {
274        let weight_limit = match tx_kind {
275            CheckedEthereumTxKind::Xcm => T::ReservedXcmpWeight::get(),
276        };
277        T::GasWeightMapping::weight_to_gas(weight_limit)
278    }
279
280    /// Similar to `transact` dispatch-able call that transacts an Ethereum transaction,
281    /// but not to apply it. This is to benchmark the weight overhead in addition to `gas_limit`.
282    #[cfg(feature = "runtime-benchmarks")]
283    pub fn transact_without_apply(
284        origin: OriginFor<T>,
285        tx: CheckedEthereumTx,
286    ) -> DispatchResultWithPostInfo {
287        let source = T::XcmTransactOrigin::ensure_origin(origin)?;
288        Self::do_transact(
289            T::AddressMapper::to_h160_or_default(&source).into_address(),
290            tx.into(),
291            CheckedEthereumTxKind::Xcm,
292            true,
293        )
294        .map(|(post_info, _)| post_info)
295    }
296}