pallet_moonbeam_foreign_assets/
evm.rs

1// Copyright 2025 Moonbeam Foundation.
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/>.
16extern crate alloc;
17
18use crate::{AssetId, Error, Pallet};
19use alloc::format;
20use ethereum_types::{BigEndianHash, H160, H256, U256};
21use fp_evm::{ExitReason, ExitSucceed};
22use frame_support::ensure;
23use frame_support::pallet_prelude::Weight;
24use pallet_evm::{GasWeightMapping, Runner};
25use precompile_utils::prelude::*;
26use precompile_utils::solidity::codec::{Address, BoundedString};
27use precompile_utils::solidity::Codec;
28use precompile_utils_macro::keccak256;
29use sp_runtime::traits::ConstU32;
30use sp_runtime::{DispatchError, SaturatedConversion};
31use sp_std::vec::Vec;
32use xcm::latest::Error as XcmError;
33
34const ERC20_CALL_MAX_CALLDATA_SIZE: usize = 4 + 32 + 32; // selector + address + uint256
35const ERC20_CREATE_MAX_CALLDATA_SIZE: usize = 16 * 1024; // 16Ko
36
37// Hardcoded gas limits (from manual binary search)
38const ERC20_CREATE_GAS_LIMIT: u64 = 3_600_000; // highest failure: 3_600_000
39pub(crate) const ERC20_BURN_FROM_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
40pub(crate) const ERC20_MINT_INTO_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
41const ERC20_PAUSE_GAS_LIMIT: u64 = 160_000; // highest failure: 150_500
42pub(crate) const ERC20_TRANSFER_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
43pub(crate) const ERC20_APPROVE_GAS_LIMIT: u64 = 160_000; // highest failure: 153_000
44const ERC20_UNPAUSE_GAS_LIMIT: u64 = 160_000; // highest failure: 149_500
45pub(crate) const ERC20_BALANCE_OF_GAS_LIMIT: u64 = 160_000; // Calculated effective gas: max(used: 24276, pov: 150736, storage: 0) = 150736
46
47#[derive(Debug, PartialEq)]
48pub enum EvmError {
49	BurnFromFail(String),
50	BalanceOfFail(String),
51	ContractReturnInvalidValue,
52	DispatchError(DispatchError),
53	EvmCallFail(String),
54	MintIntoFail(String),
55	TransferFail(String),
56}
57
58impl From<DispatchError> for EvmError {
59	fn from(e: DispatchError) -> Self {
60		Self::DispatchError(e)
61	}
62}
63
64impl From<EvmError> for XcmError {
65	fn from(error: EvmError) -> XcmError {
66		match error {
67			EvmError::BurnFromFail(err) => {
68				log::debug!("BurnFromFail error: {:?}", err);
69				XcmError::FailedToTransactAsset("Erc20 contract call burnFrom fail")
70			}
71			EvmError::BalanceOfFail(err) => {
72				log::debug!("BalanceOfFail error: {:?}", err);
73				XcmError::FailedToTransactAsset("Erc20 contract call balanceOf fail")
74			}
75			EvmError::ContractReturnInvalidValue => {
76				XcmError::FailedToTransactAsset("Erc20 contract return invalid value")
77			}
78			EvmError::DispatchError(err) => {
79				log::debug!("dispatch error: {:?}", err);
80				Self::FailedToTransactAsset("storage layer error")
81			}
82			EvmError::EvmCallFail(err) => {
83				log::debug!("EvmCallFail error: {:?}", err);
84				XcmError::FailedToTransactAsset("Fail to call erc20 contract")
85			}
86			EvmError::MintIntoFail(err) => {
87				log::debug!("MintIntoFail error: {:?}", err);
88				XcmError::FailedToTransactAsset("Erc20 contract call mintInto fail+")
89			}
90			EvmError::TransferFail(err) => {
91				log::debug!("TransferFail error: {:?}", err);
92				XcmError::FailedToTransactAsset("Erc20 contract call transfer fail")
93			}
94		}
95	}
96}
97
98#[derive(Codec)]
99#[cfg_attr(test, derive(Debug))]
100struct ForeignErc20ConstructorArgs {
101	owner: Address,
102	decimals: u8,
103	symbol: BoundedString<ConstU32<64>>,
104	token_name: BoundedString<ConstU32<256>>,
105}
106
107pub(crate) struct EvmCaller<T: crate::Config>(core::marker::PhantomData<T>);
108
109impl<T: crate::Config> EvmCaller<T> {
110	/// Deploy foreign asset erc20 contract
111	pub(crate) fn erc20_create(
112		asset_id: AssetId,
113		decimals: u8,
114		symbol: &str,
115		token_name: &str,
116	) -> Result<H160, Error<T>> {
117		// Get init code
118		let mut init = Vec::with_capacity(ERC20_CREATE_MAX_CALLDATA_SIZE);
119		init.extend_from_slice(include_bytes!("../resources/foreign_erc20_initcode.bin"));
120
121		// Add constructor parameters
122		let args = ForeignErc20ConstructorArgs {
123			owner: Pallet::<T>::account_id().into(),
124			decimals,
125			symbol: symbol.into(),
126			token_name: token_name.into(),
127		};
128		let encoded_args = precompile_utils::solidity::codec::Writer::new()
129			.write(args)
130			.build();
131		// Skip size of constructor args (32 bytes)
132		init.extend_from_slice(&encoded_args[32..]);
133
134		let contract_adress = Pallet::<T>::contract_address_from_asset_id(asset_id);
135
136		let exec_info = T::EvmRunner::create_force_address(
137			Pallet::<T>::account_id(),
138			init,
139			U256::default(),
140			ERC20_CREATE_GAS_LIMIT,
141			None,
142			None,
143			None,
144			Default::default(),
145			Default::default(),
146			false,
147			false,
148			None,
149			None,
150			&<T as pallet_evm::Config>::config(),
151			contract_adress,
152		)
153		.map_err(|err| {
154			log::debug!("erc20_create (error): {:?}", err.error.into());
155			Error::<T>::Erc20ContractCreationFail
156		})?;
157
158		ensure!(
159			matches!(
160				exec_info.exit_reason,
161				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
162			),
163			Error::Erc20ContractCreationFail
164		);
165
166		Ok(contract_adress)
167	}
168
169	pub(crate) fn erc20_mint_into(
170		erc20_contract_address: H160,
171		beneficiary: H160,
172		amount: U256,
173	) -> Result<(), EvmError> {
174		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
175		// Selector
176		input.extend_from_slice(&keccak256!("mintInto(address,uint256)")[..4]);
177		// append beneficiary address
178		input.extend_from_slice(H256::from(beneficiary).as_bytes());
179		// append amount to be minted
180		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
181
182		let weight_limit: Weight =
183			T::GasWeightMapping::gas_to_weight(ERC20_MINT_INTO_GAS_LIMIT, true);
184
185		let exec_info = T::EvmRunner::call(
186			Pallet::<T>::account_id(),
187			erc20_contract_address,
188			input,
189			U256::default(),
190			ERC20_MINT_INTO_GAS_LIMIT,
191			None,
192			None,
193			None,
194			Default::default(),
195			Default::default(),
196			false,
197			false,
198			Some(weight_limit),
199			Some(0),
200			&<T as pallet_evm::Config>::config(),
201		)
202		.map_err(|err| EvmError::MintIntoFail(format!("{:?}", err.error.into())))?;
203
204		ensure!(
205			matches!(
206				exec_info.exit_reason,
207				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
208			),
209			{
210				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
211				EvmError::MintIntoFail(err)
212			}
213		);
214
215		Ok(())
216	}
217
218	pub(crate) fn erc20_transfer(
219		erc20_contract_address: H160,
220		from: H160,
221		to: H160,
222		amount: U256,
223	) -> Result<(), EvmError> {
224		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
225		// Selector
226		input.extend_from_slice(&keccak256!("transfer(address,uint256)")[..4]);
227		// append receiver address
228		input.extend_from_slice(H256::from(to).as_bytes());
229		// append amount to be transferred
230		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
231
232		let weight_limit: Weight =
233			T::GasWeightMapping::gas_to_weight(ERC20_TRANSFER_GAS_LIMIT, true);
234
235		let exec_info = T::EvmRunner::call(
236			from,
237			erc20_contract_address,
238			input,
239			U256::default(),
240			ERC20_TRANSFER_GAS_LIMIT,
241			None,
242			None,
243			None,
244			Default::default(),
245			Default::default(),
246			false,
247			false,
248			Some(weight_limit),
249			Some(0),
250			&<T as pallet_evm::Config>::config(),
251		)
252		.map_err(|err| EvmError::TransferFail(format!("{:?}", err.error.into())))?;
253
254		ensure!(
255			matches!(
256				exec_info.exit_reason,
257				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
258			),
259			{
260				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
261				EvmError::TransferFail(err)
262			}
263		);
264
265		// return value is true.
266		let bytes: [u8; 32] = U256::from(1).to_big_endian();
267
268		// Check return value to make sure not calling on empty contracts.
269		ensure!(
270			!exec_info.value.is_empty() && exec_info.value == bytes,
271			EvmError::ContractReturnInvalidValue
272		);
273
274		Ok(())
275	}
276
277	pub(crate) fn erc20_approve(
278		erc20_contract_address: H160,
279		owner: H160,
280		spender: H160,
281		amount: U256,
282	) -> Result<(), EvmError> {
283		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
284		// Selector
285		input.extend_from_slice(&keccak256!("approve(address,uint256)")[..4]);
286		// append spender address
287		input.extend_from_slice(H256::from(spender).as_bytes());
288		// append amount to be approved
289		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
290		let weight_limit: Weight =
291			T::GasWeightMapping::gas_to_weight(ERC20_APPROVE_GAS_LIMIT, true);
292
293		let exec_info = T::EvmRunner::call(
294			owner,
295			erc20_contract_address,
296			input,
297			U256::default(),
298			ERC20_APPROVE_GAS_LIMIT,
299			None,
300			None,
301			None,
302			Default::default(),
303			Default::default(),
304			false,
305			false,
306			Some(weight_limit),
307			Some(0),
308			&<T as pallet_evm::Config>::config(),
309		)
310		.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
311
312		ensure!(
313			matches!(
314				exec_info.exit_reason,
315				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
316			),
317			{
318				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
319				EvmError::EvmCallFail(err)
320			}
321		);
322
323		Ok(())
324	}
325
326	pub(crate) fn erc20_burn_from(
327		erc20_contract_address: H160,
328		who: H160,
329		amount: U256,
330	) -> Result<(), EvmError> {
331		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
332		// Selector
333		input.extend_from_slice(&keccak256!("burnFrom(address,uint256)")[..4]);
334		// append who address
335		input.extend_from_slice(H256::from(who).as_bytes());
336		// append amount to be burn
337		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
338
339		let weight_limit: Weight =
340			T::GasWeightMapping::gas_to_weight(ERC20_BURN_FROM_GAS_LIMIT, true);
341
342		let exec_info = T::EvmRunner::call(
343			Pallet::<T>::account_id(),
344			erc20_contract_address,
345			input,
346			U256::default(),
347			ERC20_BURN_FROM_GAS_LIMIT,
348			None,
349			None,
350			None,
351			Default::default(),
352			Default::default(),
353			false,
354			false,
355			Some(weight_limit),
356			Some(0),
357			&<T as pallet_evm::Config>::config(),
358		)
359		.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
360
361		ensure!(
362			matches!(
363				exec_info.exit_reason,
364				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
365			),
366			{
367				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
368				EvmError::BurnFromFail(err)
369			}
370		);
371
372		Ok(())
373	}
374
375	// Call contract selector "pause"
376	pub(crate) fn erc20_pause(asset_id: AssetId) -> Result<(), Error<T>> {
377		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
378		// Selector
379		input.extend_from_slice(&keccak256!("pause()")[..4]);
380
381		let weight_limit: Weight = T::GasWeightMapping::gas_to_weight(ERC20_PAUSE_GAS_LIMIT, true);
382
383		let exec_info = T::EvmRunner::call(
384			Pallet::<T>::account_id(),
385			Pallet::<T>::contract_address_from_asset_id(asset_id),
386			input,
387			U256::default(),
388			ERC20_PAUSE_GAS_LIMIT,
389			None,
390			None,
391			None,
392			Default::default(),
393			Default::default(),
394			false,
395			false,
396			Some(weight_limit),
397			Some(0),
398			&<T as pallet_evm::Config>::config(),
399		)
400		.map_err(|err| {
401			log::debug!("erc20_pause (error): {:?}", err.error.into());
402			Error::<T>::EvmInternalError
403		})?;
404
405		ensure!(
406			matches!(
407				exec_info.exit_reason,
408				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
409			),
410			{
411				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
412				log::debug!("erc20_pause (error): {:?}", err);
413				Error::<T>::EvmCallPauseFail
414			}
415		);
416
417		Ok(())
418	}
419
420	// Call contract selector "unpause"
421	pub(crate) fn erc20_unpause(asset_id: AssetId) -> Result<(), Error<T>> {
422		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
423		// Selector
424		input.extend_from_slice(&keccak256!("unpause()")[..4]);
425
426		let weight_limit: Weight =
427			T::GasWeightMapping::gas_to_weight(ERC20_UNPAUSE_GAS_LIMIT, true);
428
429		let exec_info = T::EvmRunner::call(
430			Pallet::<T>::account_id(),
431			Pallet::<T>::contract_address_from_asset_id(asset_id),
432			input,
433			U256::default(),
434			ERC20_UNPAUSE_GAS_LIMIT,
435			None,
436			None,
437			None,
438			Default::default(),
439			Default::default(),
440			false,
441			false,
442			Some(weight_limit),
443			Some(0),
444			&<T as pallet_evm::Config>::config(),
445		)
446		.map_err(|err| {
447			log::debug!("erc20_unpause (error): {:?}", err.error.into());
448			Error::<T>::EvmInternalError
449		})?;
450
451		ensure!(
452			matches!(
453				exec_info.exit_reason,
454				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
455			),
456			{
457				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
458				log::debug!("erc20_unpause (error): {:?}", err);
459				Error::<T>::EvmCallUnpauseFail
460			}
461		);
462
463		Ok(())
464	}
465
466	// Call contract selector "balanceOf"
467	pub(crate) fn erc20_balance_of(asset_id: AssetId, account: H160) -> Result<U256, EvmError> {
468		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
469		// Selector
470		input.extend_from_slice(&keccak256!("balanceOf(address)")[..4]);
471		// append account address
472		input.extend_from_slice(H256::from(account).as_bytes());
473
474		let exec_info = T::EvmRunner::call(
475			Pallet::<T>::account_id(),
476			Pallet::<T>::contract_address_from_asset_id(asset_id),
477			input,
478			U256::default(),
479			ERC20_BALANCE_OF_GAS_LIMIT,
480			None,
481			None,
482			None,
483			Default::default(),
484			Default::default(),
485			false,
486			false,
487			None,
488			None,
489			&<T as pallet_evm::Config>::config(),
490		)
491		.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
492
493		ensure!(
494			matches!(
495				exec_info.exit_reason,
496				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
497			),
498			{
499				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
500				EvmError::BalanceOfFail(err)
501			}
502		);
503
504		let balance = U256::from_big_endian(&exec_info.value);
505		Ok(balance)
506	}
507}
508
509fn error_on_execution_failure(reason: &ExitReason, data: &[u8]) -> String {
510	match reason {
511		ExitReason::Succeed(_) => alloc::string::String::new(),
512		ExitReason::Error(err) => format!("evm error: {err:?}"),
513		ExitReason::Fatal(err) => format!("evm fatal: {err:?}"),
514		ExitReason::Revert(_) => extract_revert_message(data),
515	}
516}
517
518/// The data should contain a UTF-8 encoded revert reason with a minimum size consisting of:
519/// error function selector (4 bytes) + offset (32 bytes) + reason string length (32 bytes)
520fn extract_revert_message(data: &[u8]) -> alloc::string::String {
521	const LEN_START: usize = 36;
522	const MESSAGE_START: usize = 68;
523	const BASE_MESSAGE: &str = "VM Exception while processing transaction: revert";
524	// Return base message if data is too short
525	if data.len() <= MESSAGE_START {
526		return BASE_MESSAGE.into();
527	}
528	// Extract message length and calculate end position
529	let message_len =
530		U256::from_big_endian(&data[LEN_START..MESSAGE_START]).saturated_into::<usize>();
531	let message_end = MESSAGE_START.saturating_add(message_len);
532	// Return base message if data is shorter than expected message end
533	if data.len() < message_end {
534		return BASE_MESSAGE.into();
535	}
536	// Extract and decode the message
537	let body = &data[MESSAGE_START..message_end];
538	match core::str::from_utf8(body) {
539		Ok(reason) => format!("{BASE_MESSAGE} {reason}"),
540		Err(_) => BASE_MESSAGE.into(),
541	}
542}