1#![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
56const 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#[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 handle.record_cost(2500)?;
95 handle.record_db_read::<Runtime>(20)?;
97 handle.record_db_read::<Runtime>(20)?;
99 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 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 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 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 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 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 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 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 Self::call(
193 handle,
194 wormhole_bridge,
195 solidity::encode_with_selector(COMPLETE_TRANSFER_WITH_PAYLOAD_SELECTOR, wormhole_vaa),
196 )?;
197
198 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 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 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
388mod storage {
395 use super::*;
396 use frame_support::{
397 storage::types::{OptionQuery, StorageValue},
398 traits::StorageInstance,
399 };
400
401 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 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 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}