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

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

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

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

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

            
165
34
		Ok(contract_adress)
166
34
	}
167

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

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

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

            
202
19
		ensure!(
203
			matches!(
204
19
				exec_info.exit_reason,
205
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
206
			),
207
			{
208
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
209
				EvmError::MintIntoFail(err)
210
			}
211
		);
212

            
213
19
		Ok(())
214
19
	}
215

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

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

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

            
251
4
		ensure!(
252
			matches!(
253
4
				exec_info.exit_reason,
254
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
255
			),
256
			{
257
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
258
				EvmError::TransferFail(err)
259
			}
260
		);
261

            
262
		// return value is true.
263
4
		let bytes: [u8; 32] = U256::from(1).to_big_endian();
264
4

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

            
271
4
		Ok(())
272
4
	}
273

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

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

            
308
13
		ensure!(
309
			matches!(
310
13
				exec_info.exit_reason,
311
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
312
			),
313
			{
314
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
315
				EvmError::EvmCallFail(err)
316
			}
317
		);
318

            
319
13
		Ok(())
320
13
	}
321

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

            
335
29
		let weight_limit: Weight =
336
29
			T::GasWeightMapping::gas_to_weight(ERC20_BURN_FROM_GAS_LIMIT, true);
337

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

            
356
29
		ensure!(
357
			matches!(
358
29
				exec_info.exit_reason,
359
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
360
			),
361
			{
362
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
363
				EvmError::BurnFromFail(err)
364
			}
365
		);
366

            
367
29
		Ok(())
368
29
	}
369

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

            
376
5
		let weight_limit: Weight = T::GasWeightMapping::gas_to_weight(ERC20_PAUSE_GAS_LIMIT, true);
377

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

            
399
5
		ensure!(
400
			matches!(
401
5
				exec_info.exit_reason,
402
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
403
			),
404
			{
405
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
406
				log::debug!("erc20_pause (error): {:?}", err);
407
				Error::<T>::EvmCallPauseFail
408
			}
409
		);
410

            
411
5
		Ok(())
412
5
	}
413

            
414
	// Call contract selector "unpause"
415
4
	pub(crate) fn erc20_unpause(asset_id: AssetId) -> Result<(), Error<T>> {
416
4
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
417
4
		// Selector
418
4
		input.extend_from_slice(&keccak256!("unpause()")[..4]);
419
4

            
420
4
		let weight_limit: Weight =
421
4
			T::GasWeightMapping::gas_to_weight(ERC20_UNPAUSE_GAS_LIMIT, true);
422

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

            
444
4
		ensure!(
445
			matches!(
446
4
				exec_info.exit_reason,
447
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
448
			),
449
			{
450
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
451
				log::debug!("erc20_unpause (error): {:?}", err);
452
				Error::<T>::EvmCallUnpauseFail
453
			}
454
		);
455

            
456
4
		Ok(())
457
4
	}
458

            
459
	// Call contract selector "balanceOf"
460
12
	pub(crate) fn erc20_balance_of(asset_id: AssetId, account: H160) -> Result<U256, EvmError> {
461
12
		let mut input = Vec::with_capacity(ERC20_CALL_MAX_CALLDATA_SIZE);
462
12
		// Selector
463
12
		input.extend_from_slice(&keccak256!("balanceOf(address)")[..4]);
464
12
		// append account address
465
12
		input.extend_from_slice(H256::from(account).as_bytes());
466

            
467
12
		let exec_info = T::EvmRunner::call(
468
12
			Pallet::<T>::account_id(),
469
12
			Pallet::<T>::contract_address_from_asset_id(asset_id),
470
12
			input,
471
12
			U256::default(),
472
12
			ERC20_BALANCE_OF_GAS_LIMIT,
473
12
			None,
474
12
			None,
475
12
			None,
476
12
			Default::default(),
477
12
			false,
478
12
			false,
479
12
			None,
480
12
			None,
481
12
			&<T as pallet_evm::Config>::config(),
482
12
		)
483
12
		.map_err(|err| EvmError::EvmCallFail(format!("{:?}", err.error.into())))?;
484

            
485
12
		ensure!(
486
			matches!(
487
12
				exec_info.exit_reason,
488
				ExitReason::Succeed(ExitSucceed::Returned | ExitSucceed::Stopped)
489
			),
490
			{
491
				let err = error_on_execution_failure(&exec_info.exit_reason, &exec_info.value);
492
				EvmError::BalanceOfFail(err)
493
			}
494
		);
495

            
496
12
		let balance = U256::from_big_endian(&exec_info.value);
497
12
		Ok(balance)
498
12
	}
499
}
500

            
501
fn error_on_execution_failure(reason: &ExitReason, data: &[u8]) -> String {
502
	match reason {
503
		ExitReason::Succeed(_) => alloc::string::String::new(),
504
		ExitReason::Error(err) => format!("evm error: {err:?}"),
505
		ExitReason::Fatal(err) => format!("evm fatal: {err:?}"),
506
		ExitReason::Revert(_) => extract_revert_message(data),
507
	}
508
}
509

            
510
/// The data should contain a UTF-8 encoded revert reason with a minimum size consisting of:
511
/// error function selector (4 bytes) + offset (32 bytes) + reason string length (32 bytes)
512
fn extract_revert_message(data: &[u8]) -> alloc::string::String {
513
	const LEN_START: usize = 36;
514
	const MESSAGE_START: usize = 68;
515
	const BASE_MESSAGE: &str = "VM Exception while processing transaction: revert";
516
	// Return base message if data is too short
517
	if data.len() <= MESSAGE_START {
518
		return BASE_MESSAGE.into();
519
	}
520
	// Extract message length and calculate end position
521
	let message_len =
522
		U256::from_big_endian(&data[LEN_START..MESSAGE_START]).saturated_into::<usize>();
523
	let message_end = MESSAGE_START.saturating_add(message_len);
524
	// Return base message if data is shorter than expected message end
525
	if data.len() < message_end {
526
		return BASE_MESSAGE.into();
527
	}
528
	// Extract and decode the message
529
	let body = &data[MESSAGE_START..message_end];
530
	match core::str::from_utf8(body) {
531
		Ok(reason) => format!("{BASE_MESSAGE} {reason}"),
532
		Err(_) => BASE_MESSAGE.into(),
533
	}
534
}