Skip to main content

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, DecodeWithMemTracking, 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                } if transaction.action == TransactionAction::Create => {
89                    return (true, call_count);
90                }
91                pallet_ethereum::Call::transact {
92                    transaction: EthereumTransaction::EIP2930(transaction),
93                    ..
94                } if transaction.action == TransactionAction::Create => {
95                    return (true, call_count);
96                }
97                pallet_ethereum::Call::transact {
98                    transaction: EthereumTransaction::EIP1559(transaction),
99                    ..
100                } if transaction.action == TransactionAction::Create => {
101                    return (true, call_count);
102                }
103                // Inconclusive, other calls might create contracts.
104                _ => {}
105            }
106        }
107
108        if let Some(pallet_evm::Call::create { .. } | pallet_evm::Call::create2 { .. }) =
109            call.maybe_into_evm_call()
110        {
111            return (true, call_count);
112        }
113    }
114
115    (false, call_count)
116}
117
118/// Reject contract creation, unless the account is in the current evm contract allow list.
119#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo, DecodeWithMemTracking)]
120pub struct CheckContractCreation<Runtime>(PhantomData<Runtime>);
121
122impl<Runtime> CheckContractCreation<Runtime> {
123    pub fn new() -> Self {
124        Self(PhantomData)
125    }
126}
127
128impl<Runtime> Default for CheckContractCreation<Runtime> {
129    fn default() -> Self {
130        Self::new()
131    }
132}
133
134impl<Runtime> CheckContractCreation<Runtime>
135where
136    Runtime: frame_system::Config<AccountId = EthereumAccountId>
137        + pallet_ethereum::Config
138        + pallet_evm::Config
139        + crate::Config
140        + scale_info::TypeInfo
141        + fmt::Debug
142        + Send
143        + Sync,
144    RuntimeCallFor<Runtime>:
145        MaybeIntoEthCall<Runtime> + MaybeIntoEvmCall<Runtime> + MaybeNestedCall<Runtime>,
146    Result<pallet_ethereum::RawOrigin, OriginFor<Runtime>>: From<OriginFor<Runtime>>,
147    <RuntimeCallFor<Runtime> as Dispatchable>::RuntimeOrigin:
148        AsSystemOriginSigner<AccountIdFor<Runtime>> + Clone,
149{
150    pub(crate) fn do_validate_unsigned(
151        call: &RuntimeCallFor<Runtime>,
152    ) -> Result<(ValidTransaction, u32), TransactionValidityError> {
153        let (is_allowed, call_count) = is_create_unsigned_contract_allowed::<Runtime>(call);
154        if !is_allowed {
155            Err(InvalidTransaction::Custom(ERR_CONTRACT_CREATION_NOT_ALLOWED).into())
156        } else {
157            Ok((ValidTransaction::default(), call_count))
158        }
159    }
160
161    pub(crate) fn do_validate_signed(
162        origin: &OriginFor<Runtime>,
163        call: &RuntimeCallFor<Runtime>,
164    ) -> Result<(ValidTransaction, u32), TransactionValidityError> {
165        let Some(who) = origin.as_system_origin_signer() else {
166            // Reject unsigned contract creation unless anyone is allowed to create them.
167            return Self::do_validate_unsigned(call);
168        };
169
170        // Reject contract creation unless the account is in the allow list.
171        let (is_allowed, call_count) = is_create_contract_allowed::<Runtime>(call, who);
172        if !is_allowed {
173            Err(InvalidTransaction::Custom(ERR_CONTRACT_CREATION_NOT_ALLOWED).into())
174        } else {
175            Ok((ValidTransaction::default(), call_count))
176        }
177    }
178
179    pub fn get_weights(n: u32) -> Weight {
180        SubstrateWeightInfo::<Runtime>::evm_contract_check_multiple(n)
181            .max(SubstrateWeightInfo::<Runtime>::evm_contract_check_nested(n))
182    }
183}
184
185/// Data passed from prepare to post_dispatch.
186#[derive(RuntimeDebugNoBound)]
187pub enum Pre {
188    /// Refund this exact amount of weight.
189    Refund(Weight),
190}
191
192/// Data passed from validate to prepare.
193#[derive(RuntimeDebugNoBound)]
194pub enum Val {
195    /// Partially refund, based on the actual number of calls.
196    PartialRefund(u32),
197}
198
199// Unsigned calls can't create contracts. Only pallet-evm and pallet-ethereum can create contracts.
200// For pallet-evm all contracts are signed extrinsics, for pallet-ethereum there is only one
201// extrinsic that is self-contained.
202impl<Runtime> TransactionExtension<RuntimeCallFor<Runtime>> for CheckContractCreation<Runtime>
203where
204    Runtime: frame_system::Config<AccountId = EthereumAccountId>
205        + pallet_ethereum::Config
206        + pallet_evm::Config
207        + crate::Config
208        + scale_info::TypeInfo
209        + fmt::Debug
210        + Send
211        + Sync,
212    RuntimeCallFor<Runtime>:
213        MaybeIntoEthCall<Runtime> + MaybeIntoEvmCall<Runtime> + MaybeNestedCall<Runtime>,
214    Result<pallet_ethereum::RawOrigin, OriginFor<Runtime>>: From<OriginFor<Runtime>>,
215    <RuntimeCallFor<Runtime> as Dispatchable>::RuntimeOrigin:
216        AsSystemOriginSigner<AccountIdFor<Runtime>> + Clone,
217    for<'a> &'a mut PostDispatchInfoOf<RuntimeCallFor<Runtime>>: Into<&'a mut PostDispatchInfo>,
218{
219    const IDENTIFIER: &'static str = "CheckContractCreation";
220    type Implicit = ();
221    type Val = Val;
222    type Pre = Pre;
223
224    fn weight(&self, _: &RuntimeCallFor<Runtime>) -> Weight {
225        Self::get_weights(MAXIMUM_NUMBER_OF_CALLS)
226    }
227
228    fn validate(
229        &self,
230        origin: OriginFor<Runtime>,
231        call: &RuntimeCallFor<Runtime>,
232        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
233        _len: usize,
234        _self_implicit: Self::Implicit,
235        _inherited_implication: &impl Encode,
236        _source: TransactionSource,
237    ) -> ValidateResult<Self::Val, RuntimeCallFor<Runtime>> {
238        let (validity, val) = if origin.as_system_origin_signer().is_some() {
239            let (valid, call_count) = Self::do_validate_signed(&origin, call)?;
240            (valid, Val::PartialRefund(call_count))
241        } else {
242            let (valid, call_count) = Self::do_validate_unsigned(call)?;
243            (valid, Val::PartialRefund(call_count))
244        };
245
246        Ok((validity, val, origin))
247    }
248
249    fn prepare(
250        self,
251        val: Self::Val,
252        _origin: &DispatchOriginOf<RuntimeCallFor<Runtime>>,
253        _call: &RuntimeCallFor<Runtime>,
254        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
255        _len: usize,
256    ) -> Result<Self::Pre, TransactionValidityError> {
257        let pre_dispatch_weights = Self::get_weights(MAXIMUM_NUMBER_OF_CALLS);
258        match val {
259            // Refund any extra call weight
260            // TODO: use frame_system::Pallet::<Runtime>::reclaim_weight when we upgrade to 40.1.0
261            // See <https://github.com/paritytech/polkadot-sdk/blob/292368d05eec5d6649607251ab21ed2c96ebd158/cumulus/pallets/weight-reclaim/src/lib.rs#L178>
262            Val::PartialRefund(calls) => {
263                let actual_weights = Self::get_weights(calls);
264                Ok(Pre::Refund(
265                    pre_dispatch_weights.saturating_sub(actual_weights),
266                ))
267            }
268        }
269    }
270
271    fn post_dispatch_details(
272        pre: Self::Pre,
273        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
274        _post_info: &PostDispatchInfoOf<RuntimeCallFor<Runtime>>,
275        _len: usize,
276        _result: &DispatchResult,
277    ) -> Result<Weight, TransactionValidityError> {
278        let Pre::Refund(weight) = pre;
279        Ok(weight)
280    }
281
282    fn bare_validate(
283        call: &RuntimeCallFor<Runtime>,
284        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
285        _len: usize,
286    ) -> TransactionValidity {
287        Self::do_validate_unsigned(call).map(|(validity, _call_count)| validity)
288    }
289
290    fn bare_validate_and_prepare(
291        call: &RuntimeCallFor<Runtime>,
292        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
293        _len: usize,
294    ) -> Result<(), TransactionValidityError> {
295        Self::do_validate_unsigned(call)?;
296        Ok(())
297    }
298
299    // Weights for bare calls are calculated in the runtime, and excess weight refunded here.
300    fn bare_post_dispatch(
301        _info: &DispatchInfoOf<RuntimeCallFor<Runtime>>,
302        post_info: &mut PostDispatchInfoOf<RuntimeCallFor<Runtime>>,
303        _len: usize,
304        _result: &DispatchResult,
305    ) -> Result<(), TransactionValidityError> {
306        let pre_dispatch_weights = Self::get_weights(MAXIMUM_NUMBER_OF_CALLS);
307        // The number of Ethereum calls in a RuntimeCall is always 1, this is checked by
308        // is_self_contained() in the runtime.
309        let actual_weights = Self::get_weights(1);
310
311        // TODO: use frame_system::Pallet::<Runtime>::reclaim_weight when we upgrade to 40.1.0
312        let unspent = pre_dispatch_weights.saturating_sub(actual_weights);
313
314        // If we overcharged the weight, refund the extra weight.
315        let post_info = Into::<&mut PostDispatchInfo>::into(post_info);
316        if let Some(actual_weight) = post_info.actual_weight
317            && actual_weight.ref_time() >= pre_dispatch_weights.ref_time()
318        {
319            post_info.refund(unspent);
320        }
321
322        Ok(())
323    }
324}