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

            
19
use account::SYSTEM_ACCOUNT_SIZE;
20
use evm::ExitReason;
21
use fp_evm::{
22
	Context, PrecompileFailure, PrecompileHandle, Transfer, ACCOUNT_CODES_METADATA_PROOF_SIZE,
23
};
24
use frame_support::dispatch::{GetDispatchInfo, PostDispatchInfo};
25
use pallet_balances::Call as BalancesCall;
26
use pallet_evm::AddressMapping;
27
use pallet_proxy::Call as ProxyCall;
28
use pallet_proxy::Pallet as ProxyPallet;
29
use precompile_utils::precompile_set::{self, AddressType, SelectorFilter};
30
use precompile_utils::prelude::*;
31
use sp_core::{Get, H160, U256};
32
use sp_runtime::{
33
	codec::Decode,
34
	traits::{ConstU32, Dispatchable, StaticLookup, Zero},
35
	SaturatedConversion,
36
};
37
use sp_std::marker::PhantomData;
38

            
39
#[cfg(test)]
40
pub mod mock;
41
#[cfg(test)]
42
mod tests;
43

            
44
#[derive(Debug)]
45
pub struct OnlyIsProxy<Runtime>(PhantomData<Runtime>);
46

            
47
impl<Runtime> SelectorFilter for OnlyIsProxy<Runtime>
48
where
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)]
78
pub struct OnlyIsProxyAndProxy<Runtime>(PhantomData<Runtime>);
79

            
80
impl<Runtime> SelectorFilter for OnlyIsProxyAndProxy<Runtime>
81
where
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
2
	fn is_allowed(_caller: H160, selector: Option<u32>) -> bool {
97
2
		match selector {
98
			None => false,
99
2
			Some(selector) => {
100
2
				ProxyPrecompileCall::<Runtime>::is_proxy_selectors().contains(&selector)
101
2
					|| ProxyPrecompileCall::<Runtime>::proxy_selectors().contains(&selector)
102
1
					|| ProxyPrecompileCall::<Runtime>::proxy_force_type_selectors()
103
1
						.contains(&selector)
104
			}
105
		}
106
2
	}
107

            
108
	fn description() -> String {
109
		"Allowed for all callers only for selectors 'is_proxy', 'proxy', 'proxy_force_type'".into()
110
	}
111
}
112

            
113
pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
114

            
115
type GetCallDataLimit = ConstU32<CALL_DATA_LIMIT>;
116

            
117
pub 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.
125
pub 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.
139
pub struct ProxyPrecompile<Runtime>(PhantomData<Runtime>);
140

            
141
200
#[precompile_utils::precompile]
142
impl<Runtime> ProxyPrecompile<Runtime>
143
where
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
8
	fn add_proxy(
167
8
		handle: &mut impl PrecompileHandle,
168
8
		delegate: Address,
169
8
		proxy_type: u8,
170
8
		delay: u32,
171
8
	) -> EvmResult {
172
8
		let delegate = Runtime::AddressMapping::into_account_id(delegate.into());
173
8
		let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice())
174
8
			.map_err(|_| {
175
1
				RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType")
176
8
			})?;
177
7
		let delay = delay.into();
178
7

            
179
7
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
180
7

            
181
7
		// Disallow re-adding proxy via precompile to prevent re-entrancy.
182
7
		// See: https://github.com/PureStake/sr-moonbeam/issues/30
183
7
		// Note: It is also assumed that EVM calls are only allowed through `Origin::Root` and
184
7
		// filtered via CallFilter
185
7
		// Proxies:
186
7
		// Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16)
187
7
		handle.record_db_read::<Runtime>(
188
7
			28 + (29 * (<Runtime as pallet_proxy::Config>::MaxProxies::get() as usize)) + 16,
189
7
		)?;
190
7
		if ProxyPallet::<Runtime>::proxies(origin.clone())
191
7
			.0
192
7
			.iter()
193
7
			.any(|pd| pd.delegate == delegate)
194
		{
195
4
			return Err(revert("Cannot add more than one proxy"));
196
3
		}
197
3

            
198
3
		let delegate: <Runtime::Lookup as StaticLookup>::Source =
199
3
			Runtime::Lookup::unlookup(delegate.clone());
200
3
		let call: ProxyCall<Runtime> = ProxyCall::<Runtime>::add_proxy {
201
3
			delegate,
202
3
			proxy_type,
203
3
			delay,
204
3
		}
205
3
		.into();
206
3

            
207
3
		<RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
208

            
209
3
		Ok(())
210
8
	}
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
3
	fn remove_proxy(
221
3
		handle: &mut impl PrecompileHandle,
222
3
		delegate: Address,
223
3
		proxy_type: u8,
224
3
		delay: u32,
225
3
	) -> EvmResult {
226
3
		let delegate = Runtime::AddressMapping::into_account_id(delegate.into());
227
3
		let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice())
228
3
			.map_err(|_| {
229
1
				RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType")
230
3
			})?;
231
2
		let delay = delay.into();
232
2

            
233
2
		let delegate: <Runtime::Lookup as StaticLookup>::Source =
234
2
			Runtime::Lookup::unlookup(delegate.clone());
235
2
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
236
2
		let call: ProxyCall<Runtime> = ProxyCall::<Runtime>::remove_proxy {
237
2
			delegate,
238
2
			proxy_type,
239
2
			delay,
240
2
		}
241
2
		.into();
242
2

            
243
2
		<RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
244

            
245
1
		Ok(())
246
3
	}
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
2
	fn remove_proxies(handle: &mut impl PrecompileHandle) -> EvmResult {
254
2
		let origin = Runtime::AddressMapping::into_account_id(handle.context().caller);
255
2
		let call: ProxyCall<Runtime> = ProxyCall::<Runtime>::remove_proxies {}.into();
256
2

            
257
2
		<RuntimeHelper<Runtime>>::try_dispatch(handle, Some(origin).into(), call, 0)?;
258

            
259
2
		Ok(())
260
2
	}
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
5
	fn proxy(
272
5
		handle: &mut impl PrecompileHandle,
273
5
		real: Address,
274
5
		call_to: Address,
275
5
		call_data: BoundedBytes<GetCallDataLimit>,
276
5
	) -> EvmResult {
277
5
		let evm_subcall = EvmSubCall {
278
5
			to: call_to,
279
5
			value: handle.context().apparent_value,
280
5
			call_data,
281
5
		};
282
5

            
283
5
		Self::inner_proxy(handle, real, None, evm_subcall)
284
5
	}
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
1
	fn proxy_force_type(
298
1
		handle: &mut impl PrecompileHandle,
299
1
		real: Address,
300
1
		force_proxy_type: u8,
301
1
		call_to: Address,
302
1
		call_data: BoundedBytes<GetCallDataLimit>,
303
1
	) -> EvmResult {
304
1
		let proxy_type = Runtime::ProxyType::decode(&mut force_proxy_type.to_le_bytes().as_slice())
305
1
			.map_err(|_| {
306
1
				RevertReason::custom("Failed decoding value to ProxyType")
307
1
					.in_field("forceProxyType")
308
1
			})?;
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
1
	}
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
5
	fn is_proxy(
328
5
		handle: &mut impl PrecompileHandle,
329
5
		real: Address,
330
5
		delegate: Address,
331
5
		proxy_type: u8,
332
5
		delay: u32,
333
5
	) -> EvmResult<bool> {
334
5
		let delegate = Runtime::AddressMapping::into_account_id(delegate.into());
335
5
		let proxy_type = Runtime::ProxyType::decode(&mut proxy_type.to_le_bytes().as_slice())
336
5
			.map_err(|_| {
337
				RevertReason::custom("Failed decoding value to ProxyType").in_field("proxyType")
338
5
			})?;
339
5
		let delay = delay.into();
340
5

            
341
5
		let real = Runtime::AddressMapping::into_account_id(real.into());
342
5

            
343
5
		// Proxies:
344
5
		// Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16)
345
5
		handle.record_db_read::<Runtime>(
346
5
			28 + (29 * (<Runtime as pallet_proxy::Config>::MaxProxies::get() as usize)) + 16,
347
5
		)?;
348
5
		let is_proxy = ProxyPallet::<Runtime>::proxies(real)
349
5
			.0
350
5
			.iter()
351
5
			.any(|pd| pd.delegate == delegate && pd.proxy_type == proxy_type && pd.delay == delay);
352
5

            
353
5
		Ok(is_proxy)
354
5
	}
355

            
356
5
	fn inner_proxy(
357
5
		handle: &mut impl PrecompileHandle,
358
5
		real: Address,
359
5
		force_proxy_type: Option<<Runtime as pallet_proxy::Config>::ProxyType>,
360
5
		evm_subcall: EvmSubCall,
361
5
	) -> EvmResult {
362
5
		// Check that we only perform proxy calls on behalf of externally owned accounts
363
5
		let AddressType::EOA = precompile_set::get_address_type::<Runtime>(handle, real.into())?
364
		else {
365
1
			return Err(revert("real address must be EOA"));
366
		};
367

            
368
		// Read proxy
369
4
		let real_account_id = Runtime::AddressMapping::into_account_id(real.into());
370
4
		let who = Runtime::AddressMapping::into_account_id(handle.context().caller);
371
4
		// Proxies:
372
4
		// Twox64Concat(8) + AccountId(20) + BoundedVec(ProxyDefinition * MaxProxies) + Balance(16)
373
4
		handle.record_db_read::<Runtime>(
374
4
			28 + (29 * (<Runtime as pallet_proxy::Config>::MaxProxies::get() as usize)) + 16,
375
4
		)?;
376
2
		let def =
377
4
			pallet_proxy::Pallet::<Runtime>::find_proxy(&real_account_id, &who, force_proxy_type)
378
4
				.map_err(|_| RevertReason::custom("Not proxy"))?;
379
2
		frame_support::ensure!(def.delay.is_zero(), revert("Unannounced"));
380

            
381
		// Read subcall recipient code
382
		// AccountCodesMetadata: 16 (hash) + 20 (key) + 40 (CodeMetadata).
383
2
		handle.record_db_read::<Runtime>(ACCOUNT_CODES_METADATA_PROOF_SIZE.saturated_into())?;
384
2
		let recipient_has_code =
385
2
			pallet_evm::AccountCodesMetadata::<Runtime>::get(evm_subcall.to.0).is_some();
386
2

            
387
2
		// Apply proxy type filter
388
2
		frame_support::ensure!(
389
2
			def.proxy_type.is_evm_proxy_call_allowed(
390
2
				&evm_subcall,
391
2
				recipient_has_code,
392
2
				handle.remaining_gas()
393
2
			)?,
394
1
			revert("CallFiltered")
395
		);
396

            
397
		let EvmSubCall {
398
1
			to,
399
1
			value,
400
1
			call_data,
401
1
		} = evm_subcall;
402
1
		let address = to.0;
403
1

            
404
1
		let sub_context = Context {
405
1
			caller: real.0,
406
1
			address: address.clone(),
407
1
			apparent_value: value,
408
1
		};
409

            
410
1
		let transfer = if value.is_zero() {
411
1
			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
1
		let (reason, output) = handle.call(
442
1
			address,
443
1
			transfer,
444
1
			call_data.into(),
445
1
			Some(handle.remaining_gas()),
446
1
			false,
447
1
			&sub_context,
448
1
		);
449
1

            
450
1
		// Return subcall result
451
1
		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
1
			ExitReason::Succeed(_) => Ok(()),
459
		}
460
5
	}
461
}