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

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

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

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

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

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

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

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

            
99
pub(crate) struct EvmCaller<T: crate::Config>(core::marker::PhantomData<T>);
100

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

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

            
126
30
		let contract_adress = Pallet::<T>::contract_address_from_asset_id(asset_id);
127

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

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

            
157
30
		Ok(contract_adress)
158
30
	}
159

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

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

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

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

            
205
15
		Ok(())
206
15
	}
207

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

            
222
		let weight_limit: Weight =
223
			T::GasWeightMapping::gas_to_weight(ERC20_TRANSFER_GAS_LIMIT, true);
224

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

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

            
254
		// return value is true.
255
		let mut bytes = [0u8; 32];
256
		U256::from(1).to_big_endian(&mut bytes);
257

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

            
264
		Ok(())
265
	}
266

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

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

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

            
312
13
		Ok(())
313
13
	}
314

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

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

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

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

            
360
32
		Ok(())
361
32
	}
362

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

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

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

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

            
404
5
		Ok(())
405
5
	}
406

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

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

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

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

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

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

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