pallet_evm_tracker/
create_contract.rs

1//! Contract creation allow list implementations
2
3use crate::traits::{AccountIdFor, MaybeIntoEthCall, MaybeIntoEvmCall};
4use crate::weights::pallet_evm_tracker::WeightInfo as SubstrateWeightInfo;
5use crate::{MAXIMUM_NUMBER_OF_CALLS, WeightInfo};
6use domain_runtime_primitives::{ERR_CONTRACT_CREATION_NOT_ALLOWED, EthereumAccountId};
7use frame_support::RuntimeDebugNoBound;
8use frame_support::dispatch::PostDispatchInfo;
9use frame_support::pallet_prelude::{DispatchResult, PhantomData, TypeInfo};
10use frame_system::pallet_prelude::{OriginFor, RuntimeCallFor};
11use pallet_ethereum::{Transaction as EthereumTransaction, TransactionAction};
12use parity_scale_codec::{Decode, Encode};
13use scale_info::prelude::fmt;
14use sp_runtime::traits::{
15    AsSystemOriginSigner, DispatchInfoOf, DispatchOriginOf, Dispatchable, PostDispatchInfoOf,
16    RefundWeight, TransactionExtension, ValidateResult,
17};
18use sp_runtime::transaction_validity::{
19    InvalidTransaction, TransactionSource, TransactionValidity, TransactionValidityError,
20    ValidTransaction,
21};
22use sp_weights::Weight;
23use subspace_runtime_primitives::utility::{MaybeNestedCall, nested_call_iter};
24
25/// Rejects contracts that can't be created under the current allow list.
26/// Returns false if the call is a contract call, and the account is *not* allowed to call it.
27/// Otherwise, returns true.
28pub fn is_create_contract_allowed<Runtime>(
29    call: &RuntimeCallFor<Runtime>,
30    signer: &EthereumAccountId,
31) -> (bool, u32)
32where
33    Runtime: frame_system::Config<AccountId = EthereumAccountId>
34        + pallet_ethereum::Config
35        + pallet_evm::Config
36        + crate::Config,
37    RuntimeCallFor<Runtime>:
38        MaybeIntoEthCall<Runtime> + MaybeIntoEvmCall<Runtime> + MaybeNestedCall<Runtime>,
39    Result<pallet_ethereum::RawOrigin, OriginFor<Runtime>>: From<OriginFor<Runtime>>,
40{
41    // If the account is allowed to create contracts, or it's not a contract call, return true.
42    // Only enters allocating code if this account can't create contracts.
43    if crate::Pallet::<Runtime>::is_allowed_to_create_contracts(signer) {
44        return (true, 0);
45    }
46
47    let (is_create, call_count) = is_create_contract::<Runtime>(call);
48    (!is_create, call_count)
49}
50
51/// If anyone is allowed to create contracts, allows contracts. Otherwise, rejects contracts.
52/// Returns false if the call is a contract call, and there is a specific (possibly empty) allow
53/// list. Otherwise, returns true.
54pub fn is_create_unsigned_contract_allowed<Runtime>(call: &RuntimeCallFor<Runtime>) -> (bool, u32)
55where
56    Runtime: frame_system::Config + pallet_ethereum::Config + pallet_evm::Config + crate::Config,
57    RuntimeCallFor<Runtime>:
58        MaybeIntoEthCall<Runtime> + MaybeIntoEvmCall<Runtime> + MaybeNestedCall<Runtime>,
59    Result<pallet_ethereum::RawOrigin, OriginFor<Runtime>>: From<OriginFor<Runtime>>,
60{
61    // If any account is allowed to create contracts, or it's not a contract call, return true.
62    // Only enters allocating code if there is a contract creation filter.
63    if crate::Pallet::<Runtime>::is_allowed_to_create_unsigned_contracts() {
64        return (true, 0);
65    }
66
67    let (is_create, call_count) = is_create_contract::<Runtime>(call);
68    (!is_create, call_count)
69}
70
71/// Returns true if the call is a contract creation call.
72pub fn is_create_contract<Runtime>(call: &RuntimeCallFor<Runtime>) -> (bool, u32)
73where
74    Runtime: frame_system::Config + pallet_ethereum::Config + pallet_evm::Config,
75    RuntimeCallFor<Runtime>:
76        MaybeIntoEthCall<Runtime> + MaybeIntoEvmCall<Runtime> + MaybeNestedCall<Runtime>,
77    Result<pallet_ethereum::RawOrigin, OriginFor<Runtime>>: From<OriginFor<Runtime>>,
78{
79    let mut call_count = 0;
80    for call in nested_call_iter::<Runtime>(call) {
81        call_count += 1;
82
83        if let Some(call) = call.maybe_into_eth_call() {
84            match call {
85                pallet_ethereum::Call::transact {
86                    transaction: EthereumTransaction::Legacy(transaction),
87                    ..
88                } => {
89                    if transaction.action == TransactionAction::Create {
90                        return (true, call_count);
91                    }
92                }
93                pallet_ethereum::Call::transact {
94                    transaction: EthereumTransaction::EIP2930(transaction),
95                    ..
96                } => {
97                    if transaction.action == TransactionAction::Create {
98                        return (true, call_count);
99                    }
100                }
101                pallet_ethereum::Call::transact {
102                    transaction: EthereumTransaction::EIP1559(transaction),
103                    ..
104                } => {
105                    if transaction.action == TransactionAction::Create {
106                        return (true, call_count);
107                    }
108                }
109                // Inconclusive, other calls might create contracts.
110                _ => {}
111            }
112        }
113
114        if let Some(pallet_evm::Call::create { .. } | pallet_evm::Call::create2 { .. }) =
115            call.maybe_into_evm_call()
116        {
117            return (true, call_count);
118        }
119    }
120
121    (false, call_count)
122}
123
124/// Reject contract creation, unless the account is in the current evm contract allow list.
125#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
126pub struct CheckContractCreation<Runtime>(PhantomData<Runtime>);
127
128impl<Runtime> CheckContractCreation<Runtime> {
129    pub fn new() -> Self {
130        Self(PhantomData)
131    }
132}
133
134impl<Runtime> Default for CheckContractCreation<Runtime> {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140impl<Runtime> CheckContractCreation<Runtime>
141where
142    Runtime: frame_system::Config<AccountId = EthereumAccountId>
143        + pallet_ethereum::Config
144        + pallet_evm::Config
145        + crate::Config
146        + scale_info::TypeInfo
147        + fmt::Debug
148        + Send
149        + Sync,
150    RuntimeCallFor<Runtime>:
151        MaybeIntoEthCall<Runtime> + MaybeIntoEvmCall<Runtime> + MaybeNestedCall<Runtime>,
152    Result<pallet_ethereum::RawOrigin, OriginFor<Runtime>>: From<OriginFor<Runtime>>,
153    <RuntimeCallFor<Runtime> as Dispatchable>::RuntimeOrigin:
154        AsSystemOriginSigner<AccountIdFor<Runtime>> + Clone,
155{
156    pub(crate) fn do_validate_unsigned(
157        call: &RuntimeCallFor<Runtime>,
158    ) -> Result<(ValidTransaction, u32), TransactionValidityError> {
159        let (is_allowed, call_count) = is_create_unsigned_contract_allowed::<Runtime>(call);
160        if !is_allowed {
161            Err(InvalidTransaction::Custom(ERR_CONTRACT_CREATION_NOT_ALLOWED).into())
162        } else {
163            Ok((ValidTransaction::default(), call_count))
164        }
165    }
166
167    pub(crate) fn do_validate_signed(
168        origin: &OriginFor<Runtime>,
169        call: &RuntimeCallFor<Runtime>,
170    ) -> Result<(ValidTransaction, u32), TransactionValidityError> {
171        let Some(who) = origin.as_system_origin_signer() else {
172            // Reject unsigned contract creation unless anyone is allowed to create them.
173            return Self::do_validate_unsigned(call);
174        };
175
176        // Reject contract creation unless the account is in the allow list.
177        let (is_allowed, call_count) = is_create_contract_allowed::<Runtime>(call, who);
178        if !is_allowed {
179            Err(InvalidTransaction::Custom(ERR_CONTRACT_CREATION_NOT_ALLOWED).into())
180        } else {
181            Ok((ValidTransaction::default(), call_count))
182        }
183    }
184
185    pub fn get_weights(n: u32) -> Weight {
186        SubstrateWeightInfo::<Runtime>::evm_contract_check_multiple(n)
187            .max(SubstrateWeightInfo::<Runtime>::evm_contract_check_nested(n))
188    }
189}
190
191/// Data passed from prepare to post_dispatch.
192#[derive(RuntimeDebugNoBound)]
193pub enum Pre {
194    /// Refund this exact amount of weight.
195    Refund(Weight),
196}
197
198/// Data passed from validate to prepare.
199#[derive(RuntimeDebugNoBound)]
200pub enum Val {
201    /// Partially refund, based on the actual number of calls.
202    PartialRefund(u32),
203}
204
205// Unsigned calls can't create contracts. Only pallet-evm and pallet-ethereum can create contracts.
206// For pallet-evm all contracts are signed extrinsics, for pallet-ethereum there is only one
207// extrinsic that is self-contained.
208impl<Runtime> TransactionExtension<RuntimeCallFor<Runtime>> for CheckContractCreation<Runtime>
209where
210    Runtime: frame_system::Config<AccountId = EthereumAccountId>
211        + pallet_ethereum::Config
212        + pallet_evm::Config
213        + crate::Config
214        + scale_info::TypeInfo
215        + fmt::Debug
216        + Send
217        + Sync,
218    RuntimeCallFor<Runtime>:
219        MaybeIntoEthCall<Runtime> + MaybeIntoEvmCall<Runtime> + MaybeNestedCall<Runtime>,
220    Result<pallet_ethereum::RawOrigin, OriginFor<Runtime>>: From<OriginFor<Runtime>>,
221    <RuntimeCallFor<Runtime> as Dispatchable>::RuntimeOrigin:
222        AsSystemOriginSigner<AccountIdFor<Runtime>> + Clone,
223    for<'a> &'a mut PostDispatchInfoOf<RuntimeCallFor<Runtime>>: Into<&'a mut PostDispatchInfo>,
224{
225    const IDENTIFIER: &'static str = "CheckContractCreation";
226    type Implicit = ();
227    type Val = Val;
228    type Pre = Pre;
229
230    fn weight(&self, _: &RuntimeCallFor<Runtime>) -> Weight {
231        Self::get_weights(MAXIMUM_NUMBER_OF_CALLS)
232    }
233
234    fn validate(
235        &self,
236        origin: OriginFor<Runtime>,
237        call: &RuntimeCallFor<Runtime>,
238        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
239        _len: usize,
240        _self_implicit: Self::Implicit,
241        _inherited_implication: &impl Encode,
242        _source: TransactionSource,
243    ) -> ValidateResult<Self::Val, RuntimeCallFor<Runtime>> {
244        let (validity, val) = if origin.as_system_origin_signer().is_some() {
245            let (valid, call_count) = Self::do_validate_signed(&origin, call)?;
246            (valid, Val::PartialRefund(call_count))
247        } else {
248            let (valid, call_count) = Self::do_validate_unsigned(call)?;
249            (valid, Val::PartialRefund(call_count))
250        };
251
252        Ok((validity, val, origin))
253    }
254
255    fn prepare(
256        self,
257        val: Self::Val,
258        _origin: &DispatchOriginOf<RuntimeCallFor<Runtime>>,
259        _call: &RuntimeCallFor<Runtime>,
260        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
261        _len: usize,
262    ) -> Result<Self::Pre, TransactionValidityError> {
263        let pre_dispatch_weights = Self::get_weights(MAXIMUM_NUMBER_OF_CALLS);
264        match val {
265            // Refund any extra call weight
266            // TODO: use frame_system::Pallet::<Runtime>::reclaim_weight when we upgrade to 40.1.0
267            // See <https://github.com/paritytech/polkadot-sdk/blob/292368d05eec5d6649607251ab21ed2c96ebd158/cumulus/pallets/weight-reclaim/src/lib.rs#L178>
268            Val::PartialRefund(calls) => {
269                let actual_weights = Self::get_weights(calls);
270                Ok(Pre::Refund(
271                    pre_dispatch_weights.saturating_sub(actual_weights),
272                ))
273            }
274        }
275    }
276
277    fn post_dispatch_details(
278        pre: Self::Pre,
279        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
280        _post_info: &PostDispatchInfoOf<RuntimeCallFor<Runtime>>,
281        _len: usize,
282        _result: &DispatchResult,
283    ) -> Result<Weight, TransactionValidityError> {
284        let Pre::Refund(weight) = pre;
285        Ok(weight)
286    }
287
288    fn bare_validate(
289        call: &RuntimeCallFor<Runtime>,
290        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
291        _len: usize,
292    ) -> TransactionValidity {
293        Self::do_validate_unsigned(call).map(|(validity, _call_count)| validity)
294    }
295
296    fn bare_validate_and_prepare(
297        call: &RuntimeCallFor<Runtime>,
298        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
299        _len: usize,
300    ) -> Result<(), TransactionValidityError> {
301        Self::do_validate_unsigned(call)?;
302        Ok(())
303    }
304
305    // Weights for bare calls are calculated in the runtime, and excess weight refunded here.
306    fn bare_post_dispatch(
307        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
308        post_info: &mut PostDispatchInfoOf<RuntimeCallFor<Runtime>>,
309        _len: usize,
310        _result: &DispatchResult,
311    ) -> Result<(), TransactionValidityError> {
312        let pre_dispatch_weights = Self::get_weights(MAXIMUM_NUMBER_OF_CALLS);
313        // The number of Ethereum calls in a RuntimeCall is always 1, this is checked by
314        // is_self_contained() in the runtime.
315        let actual_weights = Self::get_weights(1);
316
317        // TODO: use frame_system::Pallet::<Runtime>::reclaim_weight when we upgrade to 40.1.0
318        let unspent = pre_dispatch_weights.saturating_sub(actual_weights);
319
320        // If we overcharged the weight, refund the extra weight.
321        let post_info = Into::<&mut PostDispatchInfo>::into(post_info);
322        if let Some(actual_weight) = post_info.actual_weight
323            && actual_weight.ref_time() >= pre_dispatch_weights.ref_time()
324        {
325            post_info.refund(unspent);
326        }
327
328        Ok(())
329    }
330}