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, PartialEq)]
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
2458
#[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
136
	pub(crate) fn erc20_create(
112
136
		asset_id: AssetId,
113
136
		decimals: u8,
114
136
		symbol: &str,
115
136
		token_name: &str,
116
136
	) -> Result<H160, Error<T>> {
117
136
		// Get init code
118
136
		let mut init = Vec::with_capacity(ERC20_CREATE_MAX_CALLDATA_SIZE);
119
136
		init.extend_from_slice(include_bytes!("../resources/foreign_erc20_initcode.bin"));
120
136

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

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

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

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

            
166
136
		Ok(contract_adress)
167
136
	}
168

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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