1
// Copyright 2019-2023 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
{
82
	#[precompile::public("wormholeTransferERC20(bytes)")]
83
3
	pub fn wormhole_transfer_erc20(
84
3
		handle: &mut impl PrecompileHandle,
85
3
		wormhole_vaa: BoundedBytes<GetCallDataLimit>,
86
3
	) -> EvmResult {
87
3
		log::debug!(target: "gmp-precompile", "wormhole_vaa: {:?}", wormhole_vaa.clone());
88

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

            
101
3
		ensure_enabled()?;
102

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

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

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

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

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

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

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

            
161
			wrapped_asset
162
		};
163

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

            
324
		Ok(())
325
3
	}
326

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

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

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

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

            
350
		Ok(output)
351
	}
352
}
353

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

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

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

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

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

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

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

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