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
pub(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)]
48
pub enum EvmError {
49
	BurnFromFail(String),
50
	BalanceOfFail(String),
51
	ContractReturnInvalidValue,
52
	DispatchError(DispatchError),
53
	EvmCallFail(String),
54
	MintIntoFail(String),
55
	TransferFail(String),
56
}
57

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

            
64
impl 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
368
#[derive(Codec)]
99
#[cfg_attr(test, derive(Debug))]
100
struct ForeignErc20ConstructorArgs {
101
	owner: Address,
102
	decimals: u8,
103
	symbol: BoundedString<ConstU32<64>>,
104
	token_name: BoundedString<ConstU32<256>>,
105
}
106

            
107
pub(crate) struct EvmCaller<T: crate::Config>(core::marker::PhantomData<T>);
108

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

            
121
26
		// Add constructor parameters
122
26
		let args = ForeignErc20ConstructorArgs {
123
26
			owner: Pallet::<T>::account_id().into(),
124
26
			decimals,
125
26
			symbol: symbol.into(),
126
26
			token_name: token_name.into(),
127
26
		};
128
26
		let encoded_args = precompile_utils::solidity::codec::Writer::new()
129
26
			.write(args)
130
26
			.build();
131
26
		// Skip size of constructor args (32 bytes)
132
26
		init.extend_from_slice(&encoded_args[32..]);
133
26

            
134
26
		let contract_adress = Pallet::<T>::contract_address_from_asset_id(asset_id);
135

            
136
26
		let exec_info = T::EvmRunner::create_force_address(
137
26
			Pallet::<T>::account_id(),
138
26
			init,
139
26
			U256::default(),
140
26
			ERC20_CREATE_GAS_LIMIT,
141
26
			None,
142
26
			None,
143
26
			None,
144
26
			Default::default(),
145
26
			Default::default(),
146
26
			false,
147
26
			false,
148
26
			None,
149
26
			None,
150
26
			&<T as pallet_evm::Config>::config(),
151
26
			contract_adress,
152
26
		)
153
26
		.map_err(|err| {
154
			log::debug!("erc20_create (error): {:?}", err.error.into());
155
			Error::<T>::Erc20ContractCreationFail
156
26
		})?;
157

            
158
26
		ensure!(
159
			matches!(
160
26
				exec_info.exit_reason,
161
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
162
			),
163
			Error::Erc20ContractCreationFail
164
		);
165

            
166
26
		Ok(contract_adress)
167
26
	}
168

            
169
21
	pub(crate) fn erc20_mint_into(
170
21
		erc20_contract_address: H160,
171
21
		beneficiary: H160,
172
21
		amount: U256,
173
21
	) -> Result<(), EvmError> {
174
21
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
175
21
		// Selector
176
21
		input.extend_from_slice(&keccak256!("mintInto(address,uint256)")[..4]);
177
21
		// append beneficiary address
178
21
		input.extend_from_slice(H256::from(beneficiary).as_bytes());
179
21
		// append amount to be minted
180
21
		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
181
21

            
182
21
		let weight_limit: Weight =
183
21
			T::GasWeightMapping::gas_to_weight(ERC20_MINT_INTO_GAS_LIMIT, true);
184

            
185
21
		let exec_info = T::EvmRunner::call(
186
21
			Pallet::<T>::account_id(),
187
21
			erc20_contract_address,
188
21
			input,
189
21
			U256::default(),
190
21
			ERC20_MINT_INTO_GAS_LIMIT,
191
21
			None,
192
21
			None,
193
21
			None,
194
21
			Default::default(),
195
21
			Default::default(),
196
21
			false,
197
21
			false,
198
21
			Some(weight_limit),
199
21
			Some(0),
200
21
			&<T as pallet_evm::Config>::config(),
201
21
		)
202
21
		.map_err(|err| EvmError::MintIntoFail(format!("{:?}", err.error.into())))?;
203

            
204
21
		ensure!(
205
			matches!(
206
21
				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
21
		Ok(())
216
21
	}
217

            
218
4
	pub(crate) fn erc20_transfer(
219
4
		erc20_contract_address: H160,
220
4
		from: H160,
221
4
		to: H160,
222
4
		amount: U256,
223
4
	) -> Result<(), EvmError> {
224
4
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
225
4
		// Selector
226
4
		input.extend_from_slice(&keccak256!("transfer(address,uint256)")[..4]);
227
4
		// append receiver address
228
4
		input.extend_from_slice(H256::from(to).as_bytes());
229
4
		// append amount to be transferred
230
4
		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
231
4

            
232
4
		let weight_limit: Weight =
233
4
			T::GasWeightMapping::gas_to_weight(ERC20_TRANSFER_GAS_LIMIT, true);
234

            
235
4
		let exec_info = T::EvmRunner::call(
236
4
			from,
237
4
			erc20_contract_address,
238
4
			input,
239
4
			U256::default(),
240
4
			ERC20_TRANSFER_GAS_LIMIT,
241
4
			None,
242
4
			None,
243
4
			None,
244
4
			Default::default(),
245
4
			Default::default(),
246
4
			false,
247
4
			false,
248
4
			Some(weight_limit),
249
4
			Some(0),
250
4
			&<T as pallet_evm::Config>::config(),
251
4
		)
252
4
		.map_err(|err| EvmError::TransferFail(format!("{:?}", err.error.into())))?;
253

            
254
4
		ensure!(
255
			matches!(
256
4
				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
4
		let bytes: [u8; 32] = U256::from(1).to_big_endian();
267
4

            
268
4
		// Check return value to make sure not calling on empty contracts.
269
4
		ensure!(
270
4
			!exec_info.value.is_empty() && exec_info.value == bytes,
271
			EvmError::ContractReturnInvalidValue
272
		);
273

            
274
4
		Ok(())
275
4
	}
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
46
	pub(crate) fn erc20_burn_from(
327
46
		erc20_contract_address: H160,
328
46
		who: H160,
329
46
		amount: U256,
330
46
	) -> Result<(), EvmError> {
331
46
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
332
46
		// Selector
333
46
		input.extend_from_slice(&keccak256!("burnFrom(address,uint256)")[..4]);
334
46
		// append who address
335
46
		input.extend_from_slice(H256::from(who).as_bytes());
336
46
		// append amount to be burn
337
46
		input.extend_from_slice(H256::from_uint(&amount).as_bytes());
338
46

            
339
46
		let weight_limit: Weight =
340
46
			T::GasWeightMapping::gas_to_weight(ERC20_BURN_FROM_GAS_LIMIT, true);
341

            
342
46
		let exec_info = T::EvmRunner::call(
343
46
			Pallet::<T>::account_id(),
344
46
			erc20_contract_address,
345
46
			input,
346
46
			U256::default(),
347
46
			ERC20_BURN_FROM_GAS_LIMIT,
348
46
			None,
349
46
			None,
350
46
			None,
351
46
			Default::default(),
352
46
			Default::default(),
353
46
			false,
354
46
			false,
355
46
			Some(weight_limit),
356
46
			Some(0),
357
46
			&<T as pallet_evm::Config>::config(),
358
46
		)
359
46
		.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
360

            
361
46
		ensure!(
362
			matches!(
363
46
				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
46
		Ok(())
373
46
	}
374

            
375
	// Call contract selector "pause"
376
5
	pub(crate) fn erc20_pause(asset_id: AssetId) -> Result<(), Error<T>> {
377
5
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
378
5
		// Selector
379
5
		input.extend_from_slice(&keccak256!("pause()")[..4]);
380
5

            
381
5
		let weight_limit: Weight = T::GasWeightMapping::gas_to_weight(ERC20_PAUSE_GAS_LIMIT, true);
382

            
383
5
		let exec_info = T::EvmRunner::call(
384
5
			Pallet::<T>::account_id(),
385
5
			Pallet::<T>::contract_address_from_asset_id(asset_id),
386
5
			input,
387
5
			U256::default(),
388
5
			ERC20_PAUSE_GAS_LIMIT,
389
5
			None,
390
5
			None,
391
5
			None,
392
5
			Default::default(),
393
5
			Default::default(),
394
5
			false,
395
5
			false,
396
5
			Some(weight_limit),
397
5
			Some(0),
398
5
			&<T as pallet_evm::Config>::config(),
399
5
		)
400
5
		.map_err(|err| {
401
			log::debug!("erc20_pause (error): {:?}", err.error.into());
402
			Error::<T>::EvmInternalError
403
5
		})?;
404

            
405
5
		ensure!(
406
			matches!(
407
5
				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
5
		Ok(())
418
5
	}
419

            
420
	// Call contract selector "unpause"
421
4
	pub(crate) fn erc20_unpause(asset_id: AssetId) -> Result<(), Error<T>> {
422
4
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
423
4
		// Selector
424
4
		input.extend_from_slice(&keccak256!("unpause()")[..4]);
425
4

            
426
4
		let weight_limit: Weight =
427
4
			T::GasWeightMapping::gas_to_weight(ERC20_UNPAUSE_GAS_LIMIT, true);
428

            
429
4
		let exec_info = T::EvmRunner::call(
430
4
			Pallet::<T>::account_id(),
431
4
			Pallet::<T>::contract_address_from_asset_id(asset_id),
432
4
			input,
433
4
			U256::default(),
434
4
			ERC20_UNPAUSE_GAS_LIMIT,
435
4
			None,
436
4
			None,
437
4
			None,
438
4
			Default::default(),
439
4
			Default::default(),
440
4
			false,
441
4
			false,
442
4
			Some(weight_limit),
443
4
			Some(0),
444
4
			&<T as pallet_evm::Config>::config(),
445
4
		)
446
4
		.map_err(|err| {
447
			log::debug!("erc20_unpause (error): {:?}", err.error.into());
448
			Error::<T>::EvmInternalError
449
4
		})?;
450

            
451
4
		ensure!(
452
			matches!(
453
4
				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
4
		Ok(())
464
4
	}
465

            
466
	// Call contract selector "balanceOf"
467
16
	pub(crate) fn erc20_balance_of(asset_id: AssetId, account: H160) -> Result<U256, EvmError> {
468
16
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
469
16
		// Selector
470
16
		input.extend_from_slice(&keccak256!("balanceOf(address)")[..4]);
471
16
		// append account address
472
16
		input.extend_from_slice(H256::from(account).as_bytes());
473

            
474
16
		let exec_info = T::EvmRunner::call(
475
16
			Pallet::<T>::account_id(),
476
16
			Pallet::<T>::contract_address_from_asset_id(asset_id),
477
16
			input,
478
16
			U256::default(),
479
16
			ERC20_BALANCE_OF_GAS_LIMIT,
480
16
			None,
481
16
			None,
482
16
			None,
483
16
			Default::default(),
484
16
			Default::default(),
485
16
			false,
486
16
			false,
487
16
			None,
488
16
			None,
489
16
			&<T as pallet_evm::Config>::config(),
490
16
		)
491
16
		.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
492

            
493
16
		ensure!(
494
			matches!(
495
16
				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
16
		let balance = U256::from_big_endian(&exec_info.value);
505
16
		Ok(balance)
506
16
	}
507
}
508

            
509
fn 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)
520
fn 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
}