1
// Copyright 2019-2025 PureStake Inc.
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
//! Precompile to receive GMP callbacks and forward to XCM
18

            
19
#![cfg_attr(not(feature = "std"), no_std)]
20

            
21
use account::SYSTEM_ACCOUNT_SIZE;
22
use evm::ExitReason;
23
use fp_evm::{Context, ExitRevert, PrecompileFailure, PrecompileHandle};
24
use frame_support::{
25
	dispatch::{GetDispatchInfo, PostDispatchInfo},
26
	sp_runtime::traits::Zero,
27
	traits::ConstU32,
28
};
29
use pallet_evm::AddressMapping;
30
use parity_scale_codec::{Decode, DecodeLimit};
31
use precompile_utils::{prelude::*, solidity::revert::revert_as_bytes};
32
use sp_core::{H160, U256};
33
use sp_runtime::traits::{Convert, Dispatchable};
34
use sp_std::boxed::Box;
35
use sp_std::{marker::PhantomData, vec::Vec};
36
use types::*;
37
use xcm::opaque::latest::{Asset, AssetId, Fungibility, WeightLimit};
38
use xcm::{VersionedAssets, VersionedLocation};
39
use xcm_primitives::{split_location_into_chain_part_and_beneficiary, AccountIdToCurrencyId};
40

            
41
#[cfg(test)]
42
mod mock;
43
#[cfg(test)]
44
mod tests;
45

            
46
pub mod types;
47

            
48
pub type SystemCallOf<Runtime> = <Runtime as frame_system::Config>::RuntimeCall;
49
pub type CurrencyIdOf<Runtime> = <Runtime as pallet_xcm_transactor::Config>::CurrencyId;
50
pub type CurrencyIdToLocationOf<Runtime> =
51
	<Runtime as pallet_xcm_transactor::Config>::CurrencyIdToLocation;
52

            
53
pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
54
type GetCallDataLimit = ConstU32<CALL_DATA_LIMIT>;
55

            
56
// fn selectors
57
const PARSE_VM_SELECTOR: u32 = 0xa9e11893_u32;
58
const PARSE_TRANSFER_WITH_PAYLOAD_SELECTOR: u32 = 0xea63738d_u32;
59
const COMPLETE_TRANSFER_WITH_PAYLOAD_SELECTOR: u32 = 0xc3f511c1_u32;
60
const WRAPPED_ASSET_SELECTOR: u32 = 0x1ff1e286_u32;
61
const CHAIN_ID_SELECTOR: u32 = 0x9a8a0592_u32;
62
const BALANCE_OF_SELECTOR: u32 = 0x70a08231_u32;
63
const TRANSFER_SELECTOR: u32 = 0xa9059cbb_u32;
64

            
65
/// Gmp precompile.
66
#[derive(Debug, Clone)]
67
pub struct GmpPrecompile<Runtime>(PhantomData<Runtime>);
68

            
69
26
#[precompile_utils::precompile]
70
impl<Runtime> GmpPrecompile<Runtime>
71
where
72
	Runtime: pallet_evm::Config
73
		+ frame_system::Config
74
		+ pallet_xcm::Config
75
		+ pallet_xcm_transactor::Config,
76
	SystemCallOf<Runtime>: Dispatchable<PostInfo = PostDispatchInfo> + Decode + GetDispatchInfo,
77
	<<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
78
		From<Option<Runtime::AccountId>>,
79
	<Runtime as frame_system::Config>::RuntimeCall: From<pallet_xcm::Call<Runtime>>,
80
	Runtime: AccountIdToCurrencyId<Runtime::AccountId, CurrencyIdOf<Runtime>>,
81
	<Runtime as pallet_evm::Config>::AddressMapping: AddressMapping<Runtime::AccountId>,
82
{
83
	#[precompile::public("wormholeTransferERC20(bytes)")]
84
3
	pub fn wormhole_transfer_erc20(
85
3
		handle: &mut impl PrecompileHandle,
86
3
		wormhole_vaa: BoundedBytes<GetCallDataLimit>,
87
3
	) -> EvmResult {
88
3
		log::debug!(target: "gmp-precompile", "wormhole_vaa: {:?}", wormhole_vaa.clone());
89

            
90
		// tally up gas cost:
91
		// 1 read for enabled flag
92
		// 2 reads for contract addresses
93
		// 2500 as fudge for computation, esp. payload decoding (TODO: benchmark?)
94
3
		handle.record_cost(2500)?;
95
		// CoreAddress: AccountId(20)
96
3
		handle.record_db_read::<Runtime>(20)?;
97
		// BridgeAddress: AccountId(20)
98
3
		handle.record_db_read::<Runtime>(20)?;
99
		// PrecompileEnabled: AccountId(1)
100
3
		handle.record_db_read::<Runtime>(1)?;
101

            
102
3
		ensure_enabled()?;
103

            
104
1
		let wormhole = storage::CoreAddress::get()
105
1
			.ok_or(RevertReason::custom("invalid wormhole core address"))?;
106

            
107
		let wormhole_bridge = storage::BridgeAddress::get()
108
			.ok_or(RevertReason::custom("invalid wormhole bridge address"))?;
109

            
110
		log::trace!(target: "gmp-precompile", "core contract: {:?}", wormhole);
111
		log::trace!(target: "gmp-precompile", "bridge contract: {:?}", wormhole_bridge);
112

            
113
		// get the wormhole VM from the provided VAA. Unfortunately, this forces us to parse
114
		// the VAA twice -- this seems to be a restriction imposed from the Wormhole contract design
115
		let output = Self::call(
116
			handle,
117
			wormhole,
118
			solidity::encode_with_selector(PARSE_VM_SELECTOR, wormhole_vaa.clone()),
119
		)?;
120
		let wormhole_vm: WormholeVM = solidity::decode_return_value(&output[..])?;
121

            
122
		// get the bridge transfer data from the wormhole VM payload
123
		let output = Self::call(
124
			handle,
125
			wormhole_bridge,
126
			solidity::encode_with_selector(
127
				PARSE_TRANSFER_WITH_PAYLOAD_SELECTOR,
128
				wormhole_vm.payload,
129
			),
130
		)?;
131
		let transfer_with_payload: WormholeTransferWithPayloadData =
132
			solidity::decode_return_value(&output[..])?;
133

            
134
		// get the chainId that is "us" according to the bridge
135
		let output = Self::call(
136
			handle,
137
			wormhole_bridge,
138
			solidity::encode_with_selector(CHAIN_ID_SELECTOR, ()),
139
		)?;
140
		let chain_id: U256 = solidity::decode_return_value(&output[..])?;
141
		log::debug!(target: "gmp-precompile", "our chain id: {:?}", chain_id);
142

            
143
		// if the token_chain is not equal to our chain_id, we expect a wrapper ERC20
144
		let asset_erc20_address = if chain_id == transfer_with_payload.token_chain.into() {
145
			Address::from(H160::from(transfer_with_payload.token_address))
146
		} else {
147
			// get the wrapper for this asset by calling wrappedAsset()
148
			let output = Self::call(
149
				handle,
150
				wormhole_bridge,
151
				solidity::encode_with_selector(
152
					WRAPPED_ASSET_SELECTOR,
153
					(
154
						transfer_with_payload.token_chain,
155
						transfer_with_payload.token_address,
156
					),
157
				),
158
			)?;
159
			let wrapped_asset: Address = solidity::decode_return_value(&output[..])?;
160
			log::debug!(target: "gmp-precompile", "wrapped token address: {:?}", wrapped_asset);
161

            
162
			wrapped_asset
163
		};
164

            
165
		// query our "before" balance (our being this precompile)
166
		let output = Self::call(
167
			handle,
168
			asset_erc20_address.into(),
169
			solidity::encode_with_selector(BALANCE_OF_SELECTOR, Address(handle.code_address())),
170
		)?;
171
		let before_amount: U256 = solidity::decode_return_value(&output[..])?;
172
		log::debug!(target: "gmp-precompile", "before balance: {}", before_amount);
173

            
174
		// our inner-most payload should be a VersionedUserAction
175
		let user_action = VersionedUserAction::decode_with_depth_limit(
176
			32,
177
			&mut transfer_with_payload.payload.as_bytes(),
178
		)
179
		.map_err(|_| RevertReason::Custom("Invalid GMP Payload".into()))?;
180
		log::debug!(target: "gmp-precompile", "user action: {:?}", user_action);
181

            
182
		let currency_account_id =
183
			Runtime::AddressMapping::into_account_id(asset_erc20_address.into());
184

            
185
		let currency_id: CurrencyIdOf<Runtime> =
186
			Runtime::account_to_currency_id(currency_account_id)
187
				.ok_or(revert("Unsupported asset, not a valid currency id"))?;
188

            
189
		// Complete a "Contract Controlled Transfer" with the given Wormhole VAA.
190
		// We need to invoke Wormhole's completeTransferWithPayload function, passing it the VAA.
191
		// Upon success, it should have transferred tokens to this precompile's address.
192
		Self::call(
193
			handle,
194
			wormhole_bridge,
195
			solidity::encode_with_selector(COMPLETE_TRANSFER_WITH_PAYLOAD_SELECTOR, wormhole_vaa),
196
		)?;
197

            
198
		// query our "after" balance (our being this precompile)
199
		let output = Self::call(
200
			handle,
201
			asset_erc20_address.into(),
202
			solidity::encode_with_selector(
203
				BALANCE_OF_SELECTOR,
204
				Address::from(handle.code_address()),
205
			),
206
		)?;
207
		let after_amount: U256 = solidity::decode_return_value(&output[..])?;
208
		log::debug!(target: "gmp-precompile", "after balance: {}", after_amount);
209

            
210
		let amount_transferred = after_amount.saturating_sub(before_amount);
211
		let amount = amount_transferred
212
			.try_into()
213
			.map_err(|_| revert("Amount overflows balance"))?;
214

            
215
		log::debug!(target: "gmp-precompile", "sending XCM via xtokens::transfer...");
216
		let call: Option<pallet_xcm::Call<Runtime>> = match user_action {
217
			VersionedUserAction::V1(action) => {
218
				log::debug!(target: "gmp-precompile", "Payload: V1");
219

            
220
				let asset = Asset {
221
					fun: Fungibility::Fungible(amount),
222
					id: AssetId(
223
						<CurrencyIdToLocationOf<Runtime>>::convert(currency_id)
224
							.ok_or(revert("Cannot convert CurrencyId into xcm asset"))?,
225
					),
226
				};
227

            
228
				let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(
229
					action
230
						.destination
231
						.try_into()
232
						.map_err(|_| revert("Invalid destination"))?,
233
				)
234
				.ok_or(revert("Invalid destination"))?;
235

            
236
				Some(pallet_xcm::Call::<Runtime>::transfer_assets {
237
					dest: Box::new(VersionedLocation::from(chain_part)),
238
					beneficiary: Box::new(VersionedLocation::from(beneficiary)),
239
					assets: Box::new(VersionedAssets::from(asset)),
240
					fee_asset_item: 0,
241
					weight_limit: WeightLimit::Unlimited,
242
				})
243
			}
244
			VersionedUserAction::V2(action) => {
245
				log::debug!(target: "gmp-precompile", "Payload: V2");
246
				// if the specified fee is more than the amount being transferred, we'll be nice to
247
				// the sender and pay them the entire amount.
248
				let fee = action.fee.min(amount_transferred);
249

            
250
				if fee > U256::zero() {
251
					let output = Self::call(
252
						handle,
253
						asset_erc20_address.into(),
254
						solidity::encode_with_selector(
255
							TRANSFER_SELECTOR,
256
							(Address::from(handle.context().caller), fee),
257
						),
258
					)?;
259
					let transferred: bool = solidity::decode_return_value(&output[..])?;
260

            
261
					if !transferred {
262
						return Err(RevertReason::custom("failed to transfer() fee").into());
263
					}
264
				}
265

            
266
				let fee = fee
267
					.try_into()
268
					.map_err(|_| revert("Fee amount overflows balance"))?;
269

            
270
				log::debug!(
271
					target: "gmp-precompile",
272
					"deducting fee from transferred amount {:?} - {:?} = {:?}",
273
					amount, fee, (amount - fee)
274
				);
275

            
276
				let remaining = amount.saturating_sub(fee);
277

            
278
				if !remaining.is_zero() {
279
					let asset = Asset {
280
						fun: Fungibility::Fungible(remaining),
281
						id: AssetId(
282
							<CurrencyIdToLocationOf<Runtime>>::convert(currency_id)
283
								.ok_or(revert("Cannot convert CurrencyId into xcm asset"))?,
284
						),
285
					};
286

            
287
					let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(
288
						action
289
							.destination
290
							.try_into()
291
							.map_err(|_| revert("Invalid destination"))?,
292
					)
293
					.ok_or(revert("Invalid destination"))?;
294

            
295
					Some(pallet_xcm::Call::<Runtime>::transfer_assets {
296
						dest: Box::new(VersionedLocation::from(chain_part)),
297
						beneficiary: Box::new(VersionedLocation::from(beneficiary)),
298
						assets: Box::new(VersionedAssets::from(asset)),
299
						fee_asset_item: 0,
300
						weight_limit: WeightLimit::Unlimited,
301
					})
302
				} else {
303
					None
304
				}
305
			}
306
		};
307

            
308
		if let Some(call) = call {
309
			log::debug!(target: "gmp-precompile", "sending xcm {:?}", call);
310
			let origin = Runtime::AddressMapping::into_account_id(handle.code_address());
311
			RuntimeHelper::<Runtime>::try_dispatch(
312
				handle,
313
				Some(origin).into(),
314
				call,
315
				SYSTEM_ACCOUNT_SIZE,
316
			)
317
			.map_err(|e| {
318
				log::debug!(target: "gmp-precompile", "error sending XCM: {:?}", e);
319
				e
320
			})?;
321
		} else {
322
			log::debug!(target: "gmp-precompile", "no call provided, no XCM transfer");
323
		}
324

            
325
		Ok(())
326
3
	}
327

            
328
	/// call the given contract / function selector and return its output. Returns Err if the EVM
329
	/// exit reason is not Succeed.
330
	fn call(
331
		handle: &mut impl PrecompileHandle,
332
		contract_address: H160,
333
		call_data: Vec<u8>,
334
	) -> EvmResult<Vec<u8>> {
335
		let sub_context = Context {
336
			caller: handle.code_address(),
337
			address: contract_address,
338
			apparent_value: U256::zero(),
339
		};
340

            
341
		log::debug!(
342
			target: "gmp-precompile",
343
			"calling {} from {} ...", contract_address, sub_context.caller,
344
		);
345

            
346
		let (reason, output) =
347
			handle.call(contract_address, None, call_data, None, false, &sub_context);
348

            
349
		ensure_exit_reason_success(reason, &output[..])?;
350

            
351
		Ok(output)
352
	}
353
}
354

            
355
fn ensure_exit_reason_success(reason: ExitReason, output: &[u8]) -> EvmResult<()> {
356
	log::trace!(target: "gmp-precompile", "reason: {:?}", reason);
357
	log::trace!(target: "gmp-precompile", "output: {:x?}", output);
358

            
359
	match reason {
360
		ExitReason::Fatal(exit_status) => Err(PrecompileFailure::Fatal { exit_status }),
361
		ExitReason::Revert(exit_status) => Err(PrecompileFailure::Revert {
362
			exit_status,
363
			output: output.into(),
364
		}),
365
		ExitReason::Error(exit_status) => Err(PrecompileFailure::Error { exit_status }),
366
		ExitReason::Succeed(_) => Ok(()),
367
	}
368
}
369

            
370
9
pub fn is_enabled() -> bool {
371
9
	match storage::PrecompileEnabled::get() {
372
6
		Some(enabled) => enabled,
373
3
		_ => false,
374
	}
375
9
}
376

            
377
6
fn ensure_enabled() -> EvmResult<()> {
378
6
	if is_enabled() {
379
2
		Ok(())
380
	} else {
381
4
		Err(PrecompileFailure::Revert {
382
4
			exit_status: ExitRevert::Reverted,
383
4
			output: revert_as_bytes("GMP Precompile is not enabled"),
384
4
		})
385
	}
386
6
}
387

            
388
/// We use pallet storage in our precompile by implementing a StorageInstance for each item we need
389
/// to store.
390
/// twox_128("gmp") => 0xb7f047395bba5df0367b45771c00de50
391
/// twox_128("CoreAddress") => 0x59ff23ff65cc809711800d9d04e4b14c
392
/// twox_128("BridgeAddress") => 0xc1586bde54b249fb7f521faf831ade45
393
/// twox_128("PrecompileEnabled") => 0x2551bba17abb82ef3498bab688e470b8
394
mod storage {
395
	use super::*;
396
	use frame_support::{
397
		storage::types::{OptionQuery, StorageValue},
398
		traits::StorageInstance,
399
	};
400

            
401
	// storage for the core contract
402
	pub struct CoreAddressStorageInstance;
403
	impl StorageInstance for CoreAddressStorageInstance {
404
		const STORAGE_PREFIX: &'static str = "CoreAddress";
405
1
		fn pallet_prefix() -> &'static str {
406
1
			"gmp"
407
1
		}
408
	}
409
	pub type CoreAddress = StorageValue<CoreAddressStorageInstance, H160, OptionQuery>;
410

            
411
	// storage for the bridge contract
412
	pub struct BridgeAddressStorageInstance;
413
	impl StorageInstance for BridgeAddressStorageInstance {
414
		const STORAGE_PREFIX: &'static str = "BridgeAddress";
415
		fn pallet_prefix() -> &'static str {
416
			"gmp"
417
		}
418
	}
419
	pub type BridgeAddress = StorageValue<BridgeAddressStorageInstance, H160, OptionQuery>;
420

            
421
	// storage for precompile enabled
422
	// None or Some(false) both mean that the precompile is disabled; only Some(true) means enabled.
423
	pub struct PrecompileEnabledStorageInstance;
424
	impl StorageInstance for PrecompileEnabledStorageInstance {
425
		const STORAGE_PREFIX: &'static str = "PrecompileEnabled";
426
14
		fn pallet_prefix() -> &'static str {
427
14
			"gmp"
428
14
		}
429
	}
430
	pub type PrecompileEnabled = StorageValue<PrecompileEnabledStorageInstance, bool, OptionQuery>;
431
}