pallet_evm_precompile_proxy/
lib.rs

1// Copyright 2019-2025 PureStake Inc.
2// This file is part of Moonbeam.
3
4// Moonbeam is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// Moonbeam is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with Moonbeam.  If not, see <http://www.gnu.org/licenses/>.
16
17#![cfg_attr(not(feature = "std"), no_std)]
18
19use account::SYSTEM_ACCOUNT_SIZE;
20use evm::ExitReason;
21use fp_evm::{
22	Context, PrecompileFailure, PrecompileHandle, Transfer, ACCOUNT_CODES_METADATA_PROOF_SIZE,
23};
24use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo};
25use pallet_balances::Call as BalancesCall;
26use pallet_evm::AddressMapping;
27use pallet_proxy::Call as ProxyCall;
28use pallet_proxy::Pallet as ProxyPallet;
29use precompile_utils::precompile_set::{self, AddressType, SelectorFilter};
30use precompile_utils::prelude::*;
31use sp_core::{Get, H160, U256};
32use sp_runtime::{
33	codec::Decode,
34	traits::{ConstU32, Dispatchable, StaticLookup, Zero},
35	SaturatedConversion,
36};
37use sp_std::marker::PhantomData;
38
39#[cfg(test)]
40pub mod mock;
41#[cfg(test)]
42mod tests;
43
44#[derive(Debug)]
45pub struct OnlyIsProxy<Runtime>(PhantomData<Runtime>);
46
47impl<Runtime> SelectorFilter for OnlyIsProxy<Runtime>
48where
49	Runtime:
50		pallet_proxy::Config + pallet_evm::Config + frame_system::Config + pallet_balances::Config,
51	<<Runtime as pallet_proxy::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
52		From<Option<Runtime::AccountId>>,
53	<Runtime as pallet_proxy::Config>::ProxyType: Decode + EvmProxyCallFilter,
54	<Runtime as frame_system::Config>::RuntimeCall:
55		Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
56	<<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
57		From<Option<Runtime::AccountId>>,
58	<Runtime as frame_system::Config>::RuntimeCall:
59		From<ProxyCall<Runtime>> + From<BalancesCall<Runtime>>,
60	<Runtime as pallet_balances::Config<()>>::Balance: TryFrom<U256> + Into<U256>,
61	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
62{
63	fn is_allowed(_caller: H160, selector: Option<u32>) -> bool {
64		match selector {
65			None => false,
66			Some(selector) => {
67				ProxyPrecompileCall::<Runtime>::is_proxy_selectors().contains(&selector)
68			}
69		}
70	}
71
72	fn description() -> String {
73		"Allowed for all callers only for selector 'is_proxy'".into()
74	}
75}
76
77#[derive(Debug)]
78pub struct OnlyIsProxyAndProxy<Runtime>(PhantomData<Runtime>);
79
80impl<Runtime> SelectorFilter for OnlyIsProxyAndProxy<Runtime>
81where
82	Runtime:
83		pallet_proxy::Config + pallet_evm::Config + frame_system::Config + pallet_balances::Config,
84	<<Runtime as pallet_proxy::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
85		From<Option<Runtime::AccountId>>,
86	<Runtime as pallet_proxy::Config>::ProxyType: Decode + EvmProxyCallFilter,
87	<Runtime as frame_system::Config>::RuntimeCall:
88		Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
89	<<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
90		From<Option<Runtime::AccountId>>,
91	<Runtime as frame_system::Config>::RuntimeCall:
92		From<ProxyCall<Runtime>> + From<BalancesCall<Runtime>>,
93	<Runtime as pallet_balances::Config<()>>::Balance: TryFrom<U256> + Into<U256>,
94	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
95{
96	fn is_allowed(_caller: H160, selector: Option<u32>) -> bool {
97		match selector {
98			None => false,
99			Some(selector) => {
100				ProxyPrecompileCall::<Runtime>::is_proxy_selectors().contains(&selector)
101					|| ProxyPrecompileCall::<Runtime>::proxy_selectors().contains(&selector)
102					|| ProxyPrecompileCall::<Runtime>::proxy_force_type_selectors()
103						.contains(&selector)
104			}
105		}
106	}
107
108	fn description() -> String {
109		"Allowed for all callers only for selectors 'is_proxy', 'proxy', 'proxy_force_type'".into()
110	}
111}
112
113pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
114
115type GetCallDataLimit = ConstU32<CALL_DATA_LIMIT>;
116
117pub struct EvmSubCall {
118	pub to: Address,
119	pub value: U256,
120	pub call_data: BoundedBytes<ConstU32<CALL_DATA_LIMIT>>,
121}
122
123/// A trait to filter if an evm subcall is allowed to be executed by a proxy account.
124/// This trait should be implemented by the `ProxyType` type configured in pallet proxy.
125pub trait EvmProxyCallFilter: Sized + Send + Sync {
126	/// If returns `false`, then the subcall will not be executed and the evm transaction will
127	/// revert with error message "CallFiltered".
128	fn is_evm_proxy_call_allowed(
129		&self,
130		_call: &EvmSubCall,
131		_recipient_has_code: bool,
132		_gas: u64,
133	) -> EvmResult<bool> {
134		Ok(false)
135	}
136}
137
138/// A precompile to wrap the functionality from pallet-proxy.
139pub struct ProxyPrecompile<Runtime>(PhantomData<Runtime>);
140
141#[precompile_utils::precompile]
142impl<Runtime> ProxyPrecompile<Runtime>
143where
144	Runtime:
145		pallet_proxy::Config + pallet_evm::Config + frame_system::Config + pallet_balances::Config,
146	<<Runtime as pallet_proxy::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
147		From<Option<Runtime::AccountId>>,
148	<Runtime as pallet_proxy::Config>::ProxyType: Decode + EvmProxyCallFilter,
149	<Runtime as frame_system::Config>::RuntimeCall:
150		Dispatchable<PostInfo = PostDispatchInfo> + GetDispatchInfo,
151	<<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
152		From<Option<Runtime::AccountId>>,
153	<Runtime as frame_system::Config>::RuntimeCall:
154		From<ProxyCall<Runtime>> + From<BalancesCall<Runtime>>,
155	<Runtime as pallet_balances::Config<()>>::Balance: TryFrom<U256> + Into<U256>,
156	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
157{
158	/// Register a proxy account for the sender that is able to make calls on its behalf.
159	/// The dispatch origin for this call must be Signed.
160	///
161	/// Parameters:
162	/// * delegate: The account that the caller would like to make a proxy.
163	/// * proxy_type: The permissions allowed for this proxy account.
164	/// * delay: The announcement period required of the initial proxy. Will generally be zero.
165	#[precompile::public("addProxy(address,uint8,uint32)")]
166	fn add_proxy(
167		handle: &mut impl PrecompileHandle,
168		delegate: Address,
169		proxy_type: u8,
170		delay: u32,
171	) -> EvmResult {
172		let delegate = Runtime::AddressMapping::into_account_id(delegate.into());
173		let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice())
174			.map_err(|_| {
175				RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType")
176			})?;
177		let delay = delay.into();
178
179		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
180
181		// Disallow re-adding proxy via precompile to prevent re-entrancy.
182		// See: https://github.com/PureStake/sr-moonbeam/issues/30
183		// Note: It is also assumed that EVM calls are only allowed through `Origin::Root` and
184		// filtered via CallFilter
185		// Proxies:
186		// Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16)
187		handle.record_db_read::<Runtime>(
188			28 + (29 * (<Runtime as pallet_proxy::Config>::MaxProxies::get() as usize)) + 16,
189		)?;
190		if ProxyPallet::<Runtime>::proxies(origin.clone())
191			.0
192			.iter()
193			.any(|pd| pd.delegate == delegate)
194		{
195			return Err(revert("Cannot add more than one proxy"));
196		}
197
198		let delegate: <Runtime::Lookup as StaticLookup>::Source =
199			Runtime::Lookup::unlookup(delegate.clone());
200		let call: ProxyCall<Runtime> = ProxyCall::<Runtime>::add_proxy {
201			delegate,
202			proxy_type,
203			delay,
204		}
205		.into();
206
207		<RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
208
209		Ok(())
210	}
211
212	/// Unregister a proxy account for the sender.
213	/// The dispatch origin for this call must be Signed.
214	///
215	/// Parameters:
216	/// * delegate: The account that the caller would like to remove as a proxy.
217	/// * proxy_type: The permissions currently enabled for the removed proxy account.
218	/// * delay: The announcement period required of the initial proxy. Will generally be zero.
219	#[precompile::public("removeProxy(address,uint8,uint32)")]
220	fn remove_proxy(
221		handle: &mut impl PrecompileHandle,
222		delegate: Address,
223		proxy_type: u8,
224		delay: u32,
225	) -> EvmResult {
226		let delegate = Runtime::AddressMapping::into_account_id(delegate.into());
227		let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice())
228			.map_err(|_| {
229				RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType")
230			})?;
231		let delay = delay.into();
232
233		let delegate: <Runtime::Lookup as StaticLookup>::Source =
234			Runtime::Lookup::unlookup(delegate.clone());
235		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
236		let call: ProxyCall<Runtime> = ProxyCall::<Runtime>::remove_proxy {
237			delegate,
238			proxy_type,
239			delay,
240		}
241		.into();
242
243		<RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
244
245		Ok(())
246	}
247
248	/// Unregister all proxy accounts for the sender.
249	/// The dispatch origin for this call must be Signed.
250	/// WARNING: This may be called on accounts created by anonymous, however if done, then the
251	/// unreserved fees will be inaccessible. All access to this account will be lost.
252	#[precompile::public("removeProxies()")]
253	fn remove_proxies(handle: &mut impl PrecompileHandle) -> EvmResult {
254		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
255		let call: ProxyCall<Runtime> = ProxyCall::<Runtime>::remove_proxies {}.into();
256
257		<RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
258
259		Ok(())
260	}
261
262	/// Dispatch the given subcall (`call_to`, `call_data`) from an account that the sender is
263	/// authorised for through `add_proxy`.
264	///
265	/// Parameters:
266	/// - `real`: The account that the proxy will make a call on behalf of.
267	/// - `call_to`: Recipient of the call to be made by the `real` account.
268	/// - `call_data`: Data of the call to be made by the `real` account.
269	#[precompile::public("proxy(address,address,bytes)")]
270	#[precompile::payable]
271	fn proxy(
272		handle: &mut impl PrecompileHandle,
273		real: Address,
274		call_to: Address,
275		call_data: BoundedBytes<GetCallDataLimit>,
276	) -> EvmResult {
277		let evm_subcall = EvmSubCall {
278			to: call_to,
279			value: handle.context().apparent_value,
280			call_data,
281		};
282
283		Self::inner_proxy(handle, real, None, evm_subcall)
284	}
285
286	/// Dispatch the given subcall (`call_to`, `call_data`) from an account that the sender is
287	/// authorised for through `add_proxy`.
288	///
289	/// Parameters:
290	/// - `real`: The account that the proxy will make a call on behalf of.
291	/// - `force_proxy_type`: Specify the exact proxy type to be used and checked for this call.
292	/// - `call_to`: Recipient of the call to be made by the `real` account.
293	/// - `call_data`: Data of the call to be made by the `real` account.
294	#[precompile::public("proxyForceType(address,uint8,address,bytes)")]
295	#[precompile::public("proxy_force_type(address,uint8,address,bytes)")]
296	#[precompile::payable]
297	fn proxy_force_type(
298		handle: &mut impl PrecompileHandle,
299		real: Address,
300		force_proxy_type: u8,
301		call_to: Address,
302		call_data: BoundedBytes<GetCallDataLimit>,
303	) -> EvmResult {
304		let proxy_type = Runtime::ProxyType::decode(&mut force_proxy_type.to_le_bytes().as_slice())
305			.map_err(|_| {
306				RevertReason::custom("Failed decoding value to ProxyType")
307					.in_field("forceProxyType")
308			})?;
309
310		let evm_subcall = EvmSubCall {
311			to: call_to,
312			value: handle.context().apparent_value,
313			call_data,
314		};
315
316		Self::inner_proxy(handle, real, Some(proxy_type), evm_subcall)
317	}
318
319	/// Checks if the caller has an account proxied with a given proxy type
320	///
321	/// Parameters:
322	/// * delegate: The account that the caller has maybe proxied
323	/// * proxyType: The permissions allowed for the proxy
324	/// * delay: The announcement period required of the initial proxy. Will generally be zero.
325	#[precompile::public("isProxy(address,address,uint8,uint32)")]
326	#[precompile::view]
327	fn is_proxy(
328		handle: &mut impl PrecompileHandle,
329		real: Address,
330		delegate: Address,
331		proxy_type: u8,
332		delay: u32,
333	) -> EvmResult<bool> {
334		let delegate = Runtime::AddressMapping::into_account_id(delegate.into());
335		let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice())
336			.map_err(|_| {
337				RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType")
338			})?;
339		let delay = delay.into();
340
341		let real = Runtime::AddressMapping::into_account_id(real.into());
342
343		// Proxies:
344		// Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16)
345		handle.record_db_read::<Runtime>(
346			28 + (29 * (<Runtime as pallet_proxy::Config>::MaxProxies::get() as usize)) + 16,
347		)?;
348		let is_proxy = ProxyPallet::<Runtime>::proxies(real)
349			.0
350			.iter()
351			.any(|pd| pd.delegate == delegate && pd.proxy_type == proxy_type && pd.delay == delay);
352
353		Ok(is_proxy)
354	}
355
356	fn inner_proxy(
357		handle: &mut impl PrecompileHandle,
358		real: Address,
359		force_proxy_type: Option<<Runtime as pallet_proxy::Config>::ProxyType>,
360		evm_subcall: EvmSubCall,
361	) -> EvmResult {
362		// Check that we only perform proxy calls on behalf of externally owned accounts
363		let AddressType::EOA = precompile_set::get_address_type::<Runtime>(handle, real.into())?
364		else {
365			return Err(revert("real address must be EOA"));
366		};
367
368		// Read proxy
369		let real_account_id = Runtime::AddressMapping::into_account_id(real.into());
370		let who = Runtime::AddressMapping::into_account_id(handle.context().caller);
371		// Proxies:
372		// Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16)
373		handle.record_db_read::<Runtime>(
374			28 + (29 * (<Runtime as pallet_proxy::Config>::MaxProxies::get() as usize)) + 16,
375		)?;
376		let def =
377			pallet_proxy::Pallet::<Runtime>::find_proxy(&real_account_id, &who, force_proxy_type)
378				.map_err(|_| RevertReason::custom("Not proxy"))?;
379		frame_support::ensure!(def.delay.is_zero(), revert("Unannounced"));
380
381		// Read subcall recipient code
382		// AccountCodesMetadata: 16 (hash) + 20 (key) + 40 (CodeMetadata).
383		handle.record_db_read::<Runtime>(ACCOUNT_CODES_METADATA_PROOF_SIZE.saturated_into())?;
384		let recipient_has_code =
385			pallet_evm::AccountCodesMetadata::<Runtime>::get(evm_subcall.to.0).is_some();
386
387		// Apply proxy type filter
388		frame_support::ensure!(
389			def.proxy_type.is_evm_proxy_call_allowed(
390				&evm_subcall,
391				recipient_has_code,
392				handle.remaining_gas()
393			)?,
394			revert("CallFiltered")
395		);
396
397		let EvmSubCall {
398			to,
399			value,
400			call_data,
401		} = evm_subcall;
402		let address = to.0;
403
404		let sub_context = Context {
405			caller: real.0,
406			address: address.clone(),
407			apparent_value: value,
408		};
409
410		let transfer = if value.is_zero() {
411			None
412		} else {
413			let contract_address: Runtime::AccountId =
414				Runtime::AddressMapping::into_account_id(handle.context().address);
415
416			// Send back funds received by the precompile.
417			RuntimeHelper::<Runtime>::try_dispatch(
418				handle,
419				Some(contract_address).into(),
420				pallet_balances::Call::<Runtime>::transfer_allow_death {
421					dest: Runtime::Lookup::unlookup(who),
422					value: {
423						let balance: <Runtime as pallet_balances::Config<()>>::Balance =
424							value.try_into().map_err(|_| PrecompileFailure::Revert {
425								exit_status: fp_evm::ExitRevert::Reverted,
426								output: sp_std::vec::Vec::new(),
427							})?;
428						balance
429					},
430				},
431				SYSTEM_ACCOUNT_SIZE,
432			)?;
433
434			Some(Transfer {
435				source: sub_context.caller,
436				target: address.clone(),
437				value,
438			})
439		};
440
441		let (reason, output) = handle.call(
442			address,
443			transfer,
444			call_data.into(),
445			Some(handle.remaining_gas()),
446			false,
447			&sub_context,
448		);
449
450		// Return subcall result
451		match reason {
452			ExitReason::Fatal(exit_status) => Err(PrecompileFailure::Fatal { exit_status }),
453			ExitReason::Revert(exit_status) => Err(PrecompileFailure::Revert {
454				exit_status,
455				output,
456			}),
457			ExitReason::Error(exit_status) => Err(PrecompileFailure::Error { exit_status }),
458			ExitReason::Succeed(_) => Ok(()),
459		}
460	}
461}