pallet_evm_precompile_gmp/
lib.rs

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
21use account::SYSTEM_ACCOUNT_SIZE;
22use evm::ExitReason;
23use fp_evm::{Context, ExitRevert, PrecompileFailure, PrecompileHandle};
24use frame_support::{
25	dispatch::{GetDispatchInfo, PostDispatchInfo},
26	sp_runtime::traits::Zero,
27	traits::ConstU32,
28};
29use pallet_evm::AddressMapping;
30use parity_scale_codec::{Decode, DecodeLimit};
31use precompile_utils::{prelude::*, solidity::revert::revert_as_bytes};
32use sp_core::{H160, U256};
33use sp_runtime::traits::{Convert, Dispatchable};
34use sp_std::boxed::Box;
35use sp_std::{marker::PhantomData, vec::Vec};
36use types::*;
37use xcm::opaque::latest::{Asset, AssetId, Fungibility, WeightLimit};
38use xcm::{VersionedAssets, VersionedLocation};
39use xcm_primitives::{split_location_into_chain_part_and_beneficiary, AccountIdToCurrencyId};
40
41#[cfg(test)]
42mod mock;
43#[cfg(test)]
44mod tests;
45
46pub mod types;
47
48pub type SystemCallOf<Runtime> = <Runtime as frame_system::Config>::RuntimeCall;
49pub type CurrencyIdOf<Runtime> = <Runtime as pallet_xcm_transactor::Config>::CurrencyId;
50pub type CurrencyIdToLocationOf<Runtime> =
51	<Runtime as pallet_xcm_transactor::Config>::CurrencyIdToLocation;
52
53pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
54type GetCallDataLimit = ConstU32<CALL_DATA_LIMIT>;
55
56// fn selectors
57const PARSE_VM_SELECTOR: u32 = 0xa9e11893_u32;
58const PARSE_TRANSFER_WITH_PAYLOAD_SELECTOR: u32 = 0xea63738d_u32;
59const COMPLETE_TRANSFER_WITH_PAYLOAD_SELECTOR: u32 = 0xc3f511c1_u32;
60const WRAPPED_ASSET_SELECTOR: u32 = 0x1ff1e286_u32;
61const CHAIN_ID_SELECTOR: u32 = 0x9a8a0592_u32;
62const BALANCE_OF_SELECTOR: u32 = 0x70a08231_u32;
63const TRANSFER_SELECTOR: u32 = 0xa9059cbb_u32;
64
65/// Gmp precompile.
66#[derive(Debug, Clone)]
67pub struct GmpPrecompile<Runtime>(PhantomData<Runtime>);
68
69#[precompile_utils::precompile]
70impl<Runtime> GmpPrecompile<Runtime>
71where
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	pub fn wormhole_transfer_erc20(
85		handle: &mut impl PrecompileHandle,
86		wormhole_vaa: BoundedBytes<GetCallDataLimit>,
87	) -> EvmResult {
88		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		handle.record_cost(2500)?;
95		// CoreAddress: AccountId(20)
96		handle.record_db_read::<Runtime>(20)?;
97		// BridgeAddress: AccountId(20)
98		handle.record_db_read::<Runtime>(20)?;
99		// PrecompileEnabled: AccountId(1)
100		handle.record_db_read::<Runtime>(1)?;
101
102		ensure_enabled()?;
103
104		let wormhole = storage::CoreAddress::get()
105			.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	}
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
355fn 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
370pub fn is_enabled() -> bool {
371	match storage::PrecompileEnabled::get() {
372		Some(enabled) => enabled,
373		_ => false,
374	}
375}
376
377fn ensure_enabled() -> EvmResult<()> {
378	if is_enabled() {
379		Ok(())
380	} else {
381		Err(PrecompileFailure::Revert {
382			exit_status: ExitRevert::Reverted,
383			output: revert_as_bytes("GMP Precompile is not enabled"),
384		})
385	}
386}
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
394mod 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		fn pallet_prefix() -> &'static str {
406			"gmp"
407		}
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		fn pallet_prefix() -> &'static str {
427			"gmp"
428		}
429	}
430	pub type PrecompileEnabled = StorageValue<PrecompileEnabledStorageInstance, bool, OptionQuery>;
431}