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/>.
16
extern crate alloc;
17

            
18
use crate::{AssetId, Error, Pallet};
19
use alloc::format;
20
use ethereum_types::{BigEndianHash, H160, H256, U256};
21
use fp_evm::{ExitReason, ExitSucceed};
22
use frame_support::ensure;
23
use frame_support::pallet_prelude::Weight;
24
use pallet_evm::{GasWeightMapping, Runner};
25
use precompile_utils::prelude::*;
26
use precompile_utils::solidity::codec::{Address, BoundedString};
27
use precompile_utils::solidity::Codec;
28
use precompile_utils_macro::keccak256;
29
use sp_runtime::traits::ConstU32;
30
use sp_runtime::{DispatchError, SaturatedConversion};
31
use sp_std::vec::Vec;
32
use xcm::latest::Error as XcmError;
33

            
34
const ERC20_CALL_MAX_CALLDATA_SIZE: usize = 4 + 32 + 32; // selector + address + uint256
35
const ERC20_CREATE_MAX_CALLDATA_SIZE: usize = 16 * 1024; // 16Ko
36

            
37
// Hardcoded gas limits (from manual binary search)
38
const ERC20_CREATE_GAS_LIMIT: u64 = 3_600_000; // highest failure: 3_600_000
39
pub(crate) const ERC20_BURN_FROM_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
40
pub(crate) const ERC20_MINT_INTO_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
41
const ERC20_PAUSE_GAS_LIMIT: u64 = 160_000; // highest failure: 150_500
42
pub(crate) const ERC20_TRANSFER_GAS_LIMIT: u64 = 160_000; // highest failure: 154_000
43
pub(crate) const ERC20_APPROVE_GAS_LIMIT: u64 = 160_000; // highest failure: 153_000
44
const ERC20_UNPAUSE_GAS_LIMIT: u64 = 160_000; // highest failure: 149_500
45

            
46
#[derive(Debug)]
47
pub enum EvmError {
48
	BurnFromFail(String),
49
	ContractReturnInvalidValue,
50
	DispatchError(DispatchError),
51
	EvmCallFail(String),
52
	MintIntoFail(String),
53
	TransferFail(String),
54
}
55

            
56
impl From<DispatchError> for EvmError {
57
	fn from(e: DispatchError) -> Self {
58
		Self::DispatchError(e)
59
	}
60
}
61

            
62
impl From<EvmError> for XcmError {
63
	fn from(error: EvmError) -> XcmError {
64
		match error {
65
			EvmError::BurnFromFail(err) => {
66
				log::debug!("BurnFromFail error: {:?}", err);
67
				XcmError::FailedToTransactAsset("Erc20 contract call burnFrom fail")
68
			}
69
			EvmError::ContractReturnInvalidValue => {
70
				XcmError::FailedToTransactAsset("Erc20 contract return invalid value")
71
			}
72
			EvmError::DispatchError(err) => {
73
				log::debug!("dispatch error: {:?}", err);
74
				Self::FailedToTransactAsset("storage layer error")
75
			}
76
			EvmError::EvmCallFail(err) => {
77
				log::debug!("EvmCallFail error: {:?}", err);
78
				XcmError::FailedToTransactAsset("Fail to call erc20 contract")
79
			}
80
			EvmError::MintIntoFail(err) => {
81
				log::debug!("MintIntoFail error: {:?}", err);
82
				XcmError::FailedToTransactAsset("Erc20 contract call mintInto fail+")
83
			}
84
			EvmError::TransferFail(err) => {
85
				log::debug!("TransferFail error: {:?}", err);
86
				XcmError::FailedToTransactAsset("Erc20 contract call transfer fail")
87
			}
88
		}
89
	}
90
}
91

            
92
467
#[derive(Codec)]
93
#[cfg_attr(test, derive(Debug))]
94
struct ForeignErc20ConstructorArgs {
95
	owner: Address,
96
	decimals: u8,
97
	symbol: BoundedString<ConstU32<64>>,
98
	token_name: BoundedString<ConstU32<256>>,
99
}
100

            
101
pub(crate) struct EvmCaller<T: crate::Config>(core::marker::PhantomData<T>);
102

            
103
impl<T: crate::Config> EvmCaller<T> {
104
	/// Deploy foreign asset erc20 contract
105
30
	pub(crate) fn erc20_create(
106
30
		asset_id: AssetId,
107
30
		decimals: u8,
108
30
		symbol: &str,
109
30
		token_name: &str,
110
30
	) -> Result<H160, Error<T>> {
111
30
		// Get init code
112
30
		let mut init = Vec::with_capacity(ERC20_CREATE_MAX_CALLDATA_SIZE);
113
30
		init.extend_from_slice(include_bytes!("../resources/foreign_erc20_initcode.bin"));
114
30

            
115
30
		// Add constructor parameters
116
30
		let args = ForeignErc20ConstructorArgs {
117
30
			owner: Pallet::<T>::account_id().into(),
118
30
			decimals,
119
30
			symbol: symbol.into(),
120
30
			token_name: token_name.into(),
121
30
		};
122
30
		let encoded_args = precompile_utils::solidity::codec::Writer::new()
123
30
			.write(args)
124
30
			.build();
125
30
		// Skip size of constructor args (32 bytes)
126
30
		init.extend_from_slice(&encoded_args[32..]);
127
30

            
128
30
		let contract_adress = Pallet::<T>::contract_address_from_asset_id(asset_id);
129

            
130
30
		let exec_info = T::EvmRunner::create_force_address(
131
30
			Pallet::<T>::account_id(),
132
30
			init,
133
30
			U256::default(),
134
30
			ERC20_CREATE_GAS_LIMIT,
135
30
			None,
136
30
			None,
137
30
			None,
138
30
			Default::default(),
139
30
			false,
140
30
			false,
141
30
			None,
142
30
			None,
143
30
			&<T as pallet_evm::Config>::config(),
144
30
			contract_adress,
145
30
		)
146
30
		.map_err(|err| {
147
			log::debug!("erc20_create (error): {:?}", err.error.into());
148
			Error::<T>::Erc20ContractCreationFail
149
30
		})?;
150

            
151
30
		ensure!(
152
			matches!(
153
30
				exec_info.exit_reason,
154
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
155
			),
156
			Error::Erc20ContractCreationFail
157
		);
158

            
159
30
		Ok(contract_adress)
160
30
	}
161

            
162
15
	pub(crate) fn erc20_mint_into(
163
15
		erc20_contract_address: H160,
164
15
		beneficiary: H160,
165
15
		amount: U256,
166
15
	) -> Result<(), EvmError> {
167
15
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
168
15
		// Selector
169
15
		input.extend_from_slice(&keccak256!("mintInto(address,uint256)")[..4]);
170
15
		// append beneficiary address
171
15
		input.extend_from_slice(H256::from(beneficiary).as_bytes());
172
15
		// append amount to be minted
173
15
		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
174
15

            
175
15
		let weight_limit: Weight =
176
15
			T::GasWeightMapping::gas_to_weight(ERC20_MINT_INTO_GAS_LIMIT, true);
177

            
178
15
		let exec_info = T::EvmRunner::call(
179
15
			Pallet::<T>::account_id(),
180
15
			erc20_contract_address,
181
15
			input,
182
15
			U256::default(),
183
15
			ERC20_MINT_INTO_GAS_LIMIT,
184
15
			None,
185
15
			None,
186
15
			None,
187
15
			Default::default(),
188
15
			false,
189
15
			false,
190
15
			Some(weight_limit),
191
15
			Some(0),
192
15
			&<T as pallet_evm::Config>::config(),
193
15
		)
194
15
		.map_err(|err| EvmError::MintIntoFail(format!("{:?}", err.error.into())))?;
195

            
196
15
		ensure!(
197
			matches!(
198
15
				exec_info.exit_reason,
199
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
200
			),
201
			{
202
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
203
				EvmError::MintIntoFail(err)
204
			}
205
		);
206

            
207
15
		Ok(())
208
15
	}
209

            
210
	pub(crate) fn erc20_transfer(
211
		erc20_contract_address: H160,
212
		from: H160,
213
		to: H160,
214
		amount: U256,
215
	) -> Result<(), EvmError> {
216
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
217
		// Selector
218
		input.extend_from_slice(&keccak256!("transfer(address,uint256)")[..4]);
219
		// append receiver address
220
		input.extend_from_slice(H256::from(to).as_bytes());
221
		// append amount to be transferred
222
		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
223

            
224
		let weight_limit: Weight =
225
			T::GasWeightMapping::gas_to_weight(ERC20_TRANSFER_GAS_LIMIT, true);
226

            
227
		let exec_info = T::EvmRunner::call(
228
			from,
229
			erc20_contract_address,
230
			input,
231
			U256::default(),
232
			ERC20_TRANSFER_GAS_LIMIT,
233
			None,
234
			None,
235
			None,
236
			Default::default(),
237
			false,
238
			false,
239
			Some(weight_limit),
240
			Some(0),
241
			&<T as pallet_evm::Config>::config(),
242
		)
243
		.map_err(|err| EvmError::TransferFail(format!("{:?}", err.error.into())))?;
244

            
245
		ensure!(
246
			matches!(
247
				exec_info.exit_reason,
248
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
249
			),
250
			{
251
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
252
				EvmError::TransferFail(err)
253
			}
254
		);
255

            
256
		// return value is true.
257
		let bytes: [u8; 32] = U256::from(1).to_big_endian();
258

            
259
		// Check return value to make sure not calling on empty contracts.
260
		ensure!(
261
			!exec_info.value.is_empty() && exec_info.value == bytes,
262
			EvmError::ContractReturnInvalidValue
263
		);
264

            
265
		Ok(())
266
	}
267

            
268
13
	pub(crate) fn erc20_approve(
269
13
		erc20_contract_address: H160,
270
13
		owner: H160,
271
13
		spender: H160,
272
13
		amount: U256,
273
13
	) -> Result<(), EvmError> {
274
13
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
275
13
		// Selector
276
13
		input.extend_from_slice(&keccak256!("approve(address,uint256)")[..4]);
277
13
		// append spender address
278
13
		input.extend_from_slice(H256::from(spender).as_bytes());
279
13
		// append amount to be approved
280
13
		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
281
13
		let weight_limit: Weight =
282
13
			T::GasWeightMapping::gas_to_weight(ERC20_APPROVE_GAS_LIMIT, true);
283

            
284
13
		let exec_info = T::EvmRunner::call(
285
13
			owner,
286
13
			erc20_contract_address,
287
13
			input,
288
13
			U256::default(),
289
13
			ERC20_APPROVE_GAS_LIMIT,
290
13
			None,
291
13
			None,
292
13
			None,
293
13
			Default::default(),
294
13
			false,
295
13
			false,
296
13
			Some(weight_limit),
297
13
			Some(0),
298
13
			&<T as pallet_evm::Config>::config(),
299
13
		)
300
13
		.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
301

            
302
13
		ensure!(
303
			matches!(
304
13
				exec_info.exit_reason,
305
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
306
			),
307
			{
308
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
309
				EvmError::EvmCallFail(err)
310
			}
311
		);
312

            
313
13
		Ok(())
314
13
	}
315

            
316
29
	pub(crate) fn erc20_burn_from(
317
29
		erc20_contract_address: H160,
318
29
		who: H160,
319
29
		amount: U256,
320
29
	) -> Result<(), EvmError> {
321
29
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
322
29
		// Selector
323
29
		input.extend_from_slice(&keccak256!("burnFrom(address,uint256)")[..4]);
324
29
		// append who address
325
29
		input.extend_from_slice(H256::from(who).as_bytes());
326
29
		// append amount to be burn
327
29
		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
328
29

            
329
29
		let weight_limit: Weight =
330
29
			T::GasWeightMapping::gas_to_weight(ERC20_BURN_FROM_GAS_LIMIT, true);
331

            
332
29
		let exec_info = T::EvmRunner::call(
333
29
			Pallet::<T>::account_id(),
334
29
			erc20_contract_address,
335
29
			input,
336
29
			U256::default(),
337
29
			ERC20_BURN_FROM_GAS_LIMIT,
338
29
			None,
339
29
			None,
340
29
			None,
341
29
			Default::default(),
342
29
			false,
343
29
			false,
344
29
			Some(weight_limit),
345
29
			Some(0),
346
29
			&<T as pallet_evm::Config>::config(),
347
29
		)
348
29
		.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
349

            
350
29
		ensure!(
351
			matches!(
352
29
				exec_info.exit_reason,
353
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
354
			),
355
			{
356
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
357
				EvmError::BurnFromFail(err)
358
			}
359
		);
360

            
361
29
		Ok(())
362
29
	}
363

            
364
	// Call contract selector "pause"
365
5
	pub(crate) fn erc20_pause(asset_id: AssetId) -> Result<(), Error<T>> {
366
5
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
367
5
		// Selector
368
5
		input.extend_from_slice(&keccak256!("pause()")[..4]);
369
5

            
370
5
		let weight_limit: Weight = T::GasWeightMapping::gas_to_weight(ERC20_PAUSE_GAS_LIMIT, true);
371

            
372
5
		let exec_info = T::EvmRunner::call(
373
5
			Pallet::<T>::account_id(),
374
5
			Pallet::<T>::contract_address_from_asset_id(asset_id),
375
5
			input,
376
5
			U256::default(),
377
5
			ERC20_PAUSE_GAS_LIMIT,
378
5
			None,
379
5
			None,
380
5
			None,
381
5
			Default::default(),
382
5
			false,
383
5
			false,
384
5
			Some(weight_limit),
385
5
			Some(0),
386
5
			&<T as pallet_evm::Config>::config(),
387
5
		)
388
5
		.map_err(|err| {
389
			log::debug!("erc20_pause (error): {:?}", err.error.into());
390
			Error::<T>::EvmInternalError
391
5
		})?;
392

            
393
5
		ensure!(
394
			matches!(
395
5
				exec_info.exit_reason,
396
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
397
			),
398
			{
399
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
400
				log::debug!("erc20_pause (error): {:?}", err);
401
				Error::<T>::EvmCallPauseFail
402
			}
403
		);
404

            
405
5
		Ok(())
406
5
	}
407

            
408
	// Call contract selector "unpause"
409
4
	pub(crate) fn erc20_unpause(asset_id: AssetId) -> Result<(), Error<T>> {
410
4
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
411
4
		// Selector
412
4
		input.extend_from_slice(&keccak256!("unpause()")[..4]);
413
4

            
414
4
		let weight_limit: Weight =
415
4
			T::GasWeightMapping::gas_to_weight(ERC20_UNPAUSE_GAS_LIMIT, true);
416

            
417
4
		let exec_info = T::EvmRunner::call(
418
4
			Pallet::<T>::account_id(),
419
4
			Pallet::<T>::contract_address_from_asset_id(asset_id),
420
4
			input,
421
4
			U256::default(),
422
4
			ERC20_UNPAUSE_GAS_LIMIT,
423
4
			None,
424
4
			None,
425
4
			None,
426
4
			Default::default(),
427
4
			false,
428
4
			false,
429
4
			Some(weight_limit),
430
4
			Some(0),
431
4
			&<T as pallet_evm::Config>::config(),
432
4
		)
433
4
		.map_err(|err| {
434
			log::debug!("erc20_unpause (error): {:?}", err.error.into());
435
			Error::<T>::EvmInternalError
436
4
		})?;
437

            
438
4
		ensure!(
439
			matches!(
440
4
				exec_info.exit_reason,
441
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
442
			),
443
			{
444
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
445
				log::debug!("erc20_unpause (error): {:?}", err);
446
				Error::<T>::EvmCallUnpauseFail
447
			}
448
		);
449

            
450
4
		Ok(())
451
4
	}
452
}
453

            
454
fn error_on_execution_failure(reason: &ExitReason, data: &[u8]) -> String {
455
	match reason {
456
		ExitReason::Succeed(_) => alloc::string::String::new(),
457
		ExitReason::Error(err) => format!("evm error: {err:?}"),
458
		ExitReason::Fatal(err) => format!("evm fatal: {err:?}"),
459
		ExitReason::Revert(_) => extract_revert_message(data),
460
	}
461
}
462

            
463
/// The data should contain a UTF-8 encoded revert reason with a minimum size consisting of:
464
/// error function selector (4 bytes) + offset (32 bytes) + reason string length (32 bytes)
465
fn extract_revert_message(data: &[u8]) -> alloc::string::String {
466
	const LEN_START: usize = 36;
467
	const MESSAGE_START: usize = 68;
468
	const BASE_MESSAGE: &str = "VM Exception while processing transaction: revert";
469
	// Return base message if data is too short
470
	if data.len() <= MESSAGE_START {
471
		return BASE_MESSAGE.into();
472
	}
473
	// Extract message length and calculate end position
474
	let message_len =
475
		U256::from_big_endian(&data[LEN_START..MESSAGE_START]).saturated_into::<usize>();
476
	let message_end = MESSAGE_START.saturating_add(message_len);
477
	// Return base message if data is shorter than expected message end
478
	if data.len() < message_end {
479
		return BASE_MESSAGE.into();
480
	}
481
	// Extract and decode the message
482
	let body = &data[MESSAGE_START..message_end];
483
	match core::str::from_utf8(body) {
484
		Ok(reason) => format!("{BASE_MESSAGE} {reason}"),
485
		Err(_) => BASE_MESSAGE.into(),
486
	}
487
}