#![cfg_attr(not(feature = "std"), no_std)]
use account::SYSTEM_ACCOUNT_SIZE;
use evm::ExitReason;
use fp_evm::{Context, ExitRevert, PrecompileFailure, PrecompileHandle};
use frame_support::{
dispatch::{GetDispatchInfo, PostDispatchInfo},
sp_runtime::traits::Zero,
traits::ConstU32,
};
use pallet_evm::AddressMapping;
use parity_scale_codec::{Decode, DecodeLimit};
use precompile_utils::{prelude::*, solidity::revert::revert_as_bytes};
use sp_core::{H160, U256};
use sp_runtime::traits::{Convert, Dispatchable};
use sp_std::boxed::Box;
use sp_std::{marker::PhantomData, vec::Vec};
use types::*;
use xcm::opaque::latest::{Asset, AssetId, Fungibility, WeightLimit};
use xcm::{VersionedAssets, VersionedLocation};
use xcm_primitives::{split_location_into_chain_part_and_beneficiary, AccountIdToCurrencyId};
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod types;
pub type SystemCallOf<Runtime> = <Runtime as frame_system::Config>::RuntimeCall;
pub type CurrencyIdOf<Runtime> = <Runtime as pallet_xcm_transactor::Config>::CurrencyId;
pub type CurrencyIdToLocationOf<Runtime> =
<Runtime as pallet_xcm_transactor::Config>::CurrencyIdToLocation;
pub const CALL_DATA_LIMIT: u32 = 2u32.pow(16);
type GetCallDataLimit = ConstU32<CALL_DATA_LIMIT>;
const PARSE_VM_SELECTOR: u32 = 0xa9e11893_u32;
const PARSE_TRANSFER_WITH_PAYLOAD_SELECTOR: u32 = 0xea63738d_u32;
const COMPLETE_TRANSFER_WITH_PAYLOAD_SELECTOR: u32 = 0xc3f511c1_u32;
const WRAPPED_ASSET_SELECTOR: u32 = 0x1ff1e286_u32;
const CHAIN_ID_SELECTOR: u32 = 0x9a8a0592_u32;
const BALANCE_OF_SELECTOR: u32 = 0x70a08231_u32;
const TRANSFER_SELECTOR: u32 = 0xa9059cbb_u32;
#[derive(Debug, Clone)]
pub struct GmpPrecompile<Runtime>(PhantomData<Runtime>);
#[precompile_utils::precompile]
impl<Runtime> GmpPrecompile<Runtime>
where
Runtime: pallet_evm::Config
+ frame_system::Config
+ pallet_xcm::Config
+ pallet_xcm_transactor::Config,
SystemCallOf<Runtime>: Dispatchable<PostInfo = PostDispatchInfo> + Decode + GetDispatchInfo,
<<Runtime as frame_system::Config>::RuntimeCall as Dispatchable>::RuntimeOrigin:
From<Option<Runtime::AccountId>>,
<Runtime as frame_system::Config>::RuntimeCall: From<pallet_xcm::Call<Runtime>>,
Runtime: AccountIdToCurrencyId<Runtime::AccountId, CurrencyIdOf<Runtime>>,
{
#[precompile::public("wormholeTransferERC20(bytes)")]
pub fn wormhole_transfer_erc20(
handle: &mut impl PrecompileHandle,
wormhole_vaa: BoundedBytes<GetCallDataLimit>,
) -> EvmResult {
log::debug!(target: "gmp-precompile", "wormhole_vaa: {:?}", wormhole_vaa.clone());
handle.record_cost(2500)?;
handle.record_db_read::<Runtime>(20)?;
handle.record_db_read::<Runtime>(20)?;
handle.record_db_read::<Runtime>(1)?;
ensure_enabled()?;
let wormhole = storage::CoreAddress::get()
.ok_or(RevertReason::custom("invalid wormhole core address"))?;
let wormhole_bridge = storage::BridgeAddress::get()
.ok_or(RevertReason::custom("invalid wormhole bridge address"))?;
log::trace!(target: "gmp-precompile", "core contract: {:?}", wormhole);
log::trace!(target: "gmp-precompile", "bridge contract: {:?}", wormhole_bridge);
let output = Self::call(
handle,
wormhole,
solidity::encode_with_selector(PARSE_VM_SELECTOR, wormhole_vaa.clone()),
)?;
let wormhole_vm: WormholeVM = solidity::decode_return_value(&output[..])?;
let output = Self::call(
handle,
wormhole_bridge,
solidity::encode_with_selector(
PARSE_TRANSFER_WITH_PAYLOAD_SELECTOR,
wormhole_vm.payload,
),
)?;
let transfer_with_payload: WormholeTransferWithPayloadData =
solidity::decode_return_value(&output[..])?;
let output = Self::call(
handle,
wormhole_bridge,
solidity::encode_with_selector(CHAIN_ID_SELECTOR, ()),
)?;
let chain_id: U256 = solidity::decode_return_value(&output[..])?;
log::debug!(target: "gmp-precompile", "our chain id: {:?}", chain_id);
let asset_erc20_address = if chain_id == transfer_with_payload.token_chain.into() {
Address::from(H160::from(transfer_with_payload.token_address))
} else {
let output = Self::call(
handle,
wormhole_bridge,
solidity::encode_with_selector(
WRAPPED_ASSET_SELECTOR,
(
transfer_with_payload.token_chain,
transfer_with_payload.token_address,
),
),
)?;
let wrapped_asset: Address = solidity::decode_return_value(&output[..])?;
log::debug!(target: "gmp-precompile", "wrapped token address: {:?}", wrapped_asset);
wrapped_asset
};
let output = Self::call(
handle,
asset_erc20_address.into(),
solidity::encode_with_selector(BALANCE_OF_SELECTOR, Address(handle.code_address())),
)?;
let before_amount: U256 = solidity::decode_return_value(&output[..])?;
log::debug!(target: "gmp-precompile", "before balance: {}", before_amount);
let user_action = VersionedUserAction::decode_with_depth_limit(
32,
&mut transfer_with_payload.payload.as_bytes(),
)
.map_err(|_| RevertReason::Custom("Invalid GMP Payload".into()))?;
log::debug!(target: "gmp-precompile", "user action: {:?}", user_action);
let currency_account_id =
Runtime::AddressMapping::into_account_id(asset_erc20_address.into());
let currency_id: CurrencyIdOf<Runtime> =
Runtime::account_to_currency_id(currency_account_id)
.ok_or(revert("Unsupported asset, not a valid currency id"))?;
Self::call(
handle,
wormhole_bridge,
solidity::encode_with_selector(COMPLETE_TRANSFER_WITH_PAYLOAD_SELECTOR, wormhole_vaa),
)?;
let output = Self::call(
handle,
asset_erc20_address.into(),
solidity::encode_with_selector(
BALANCE_OF_SELECTOR,
Address::from(handle.code_address()),
),
)?;
let after_amount: U256 = solidity::decode_return_value(&output[..])?;
log::debug!(target: "gmp-precompile", "after balance: {}", after_amount);
let amount_transferred = after_amount.saturating_sub(before_amount);
let amount = amount_transferred
.try_into()
.map_err(|_| revert("Amount overflows balance"))?;
log::debug!(target: "gmp-precompile", "sending XCM via xtokens::transfer...");
let call: Option<pallet_xcm::Call<Runtime>> = match user_action {
VersionedUserAction::V1(action) => {
log::debug!(target: "gmp-precompile", "Payload: V1");
let asset = Asset {
fun: Fungibility::Fungible(amount),
id: AssetId(
<CurrencyIdToLocationOf<Runtime>>::convert(currency_id)
.ok_or(revert("Cannot convert CurrencyId into xcm asset"))?,
),
};
let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(
action
.destination
.try_into()
.map_err(|_| revert("Invalid destination"))?,
)
.ok_or(revert("Invalid destination"))?;
Some(pallet_xcm::Call::<Runtime>::transfer_assets {
dest: Box::new(VersionedLocation::V4(chain_part)),
beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
assets: Box::new(VersionedAssets::V4(asset.into())),
fee_asset_item: 0,
weight_limit: WeightLimit::Unlimited,
})
}
VersionedUserAction::V2(action) => {
log::debug!(target: "gmp-precompile", "Payload: V2");
let fee = action.fee.min(amount_transferred);
if fee > U256::zero() {
let output = Self::call(
handle,
asset_erc20_address.into(),
solidity::encode_with_selector(
TRANSFER_SELECTOR,
(Address::from(handle.context().caller), fee),
),
)?;
let transferred: bool = solidity::decode_return_value(&output[..])?;
if !transferred {
return Err(RevertReason::custom("failed to transfer() fee").into());
}
}
let fee = fee
.try_into()
.map_err(|_| revert("Fee amount overflows balance"))?;
log::debug!(
target: "gmp-precompile",
"deducting fee from transferred amount {:?} - {:?} = {:?}",
amount, fee, (amount - fee)
);
let remaining = amount.saturating_sub(fee);
if !remaining.is_zero() {
let asset = Asset {
fun: Fungibility::Fungible(remaining),
id: AssetId(
<CurrencyIdToLocationOf<Runtime>>::convert(currency_id)
.ok_or(revert("Cannot convert CurrencyId into xcm asset"))?,
),
};
let (chain_part, beneficiary) = split_location_into_chain_part_and_beneficiary(
action
.destination
.try_into()
.map_err(|_| revert("Invalid destination"))?,
)
.ok_or(revert("Invalid destination"))?;
Some(pallet_xcm::Call::<Runtime>::transfer_assets {
dest: Box::new(VersionedLocation::V4(chain_part)),
beneficiary: Box::new(VersionedLocation::V4(beneficiary)),
assets: Box::new(VersionedAssets::V4(asset.into())),
fee_asset_item: 0,
weight_limit: WeightLimit::Unlimited,
})
} else {
None
}
}
};
if let Some(call) = call {
log::debug!(target: "gmp-precompile", "sending xcm {:?}", call);
let origin = Runtime::AddressMapping::into_account_id(handle.code_address());
RuntimeHelper::<Runtime>::try_dispatch(
handle,
Some(origin).into(),
call,
SYSTEM_ACCOUNT_SIZE,
)
.map_err(|e| {
log::debug!(target: "gmp-precompile", "error sending XCM: {:?}", e);
e
})?;
} else {
log::debug!(target: "gmp-precompile", "no call provided, no XCM transfer");
}
Ok(())
}
fn call(
handle: &mut impl PrecompileHandle,
contract_address: H160,
call_data: Vec<u8>,
) -> EvmResult<Vec<u8>> {
let sub_context = Context {
caller: handle.code_address(),
address: contract_address,
apparent_value: U256::zero(),
};
log::debug!(
target: "gmp-precompile",
"calling {} from {} ...", contract_address, sub_context.caller,
);
let (reason, output) =
handle.call(contract_address, None, call_data, None, false, &sub_context);
ensure_exit_reason_success(reason, &output[..])?;
Ok(output)
}
}
fn ensure_exit_reason_success(reason: ExitReason, output: &[u8]) -> EvmResult<()> {
log::trace!(target: "gmp-precompile", "reason: {:?}", reason);
log::trace!(target: "gmp-precompile", "output: {:x?}", output);
match reason {
ExitReason::Fatal(exit_status) => Err(PrecompileFailure::Fatal { exit_status }),
ExitReason::Revert(exit_status) => Err(PrecompileFailure::Revert {
exit_status,
output: output.into(),
}),
ExitReason::Error(exit_status) => Err(PrecompileFailure::Error { exit_status }),
ExitReason::Succeed(_) => Ok(()),
}
}
pub fn is_enabled() -> bool {
match storage::PrecompileEnabled::get() {
Some(enabled) => enabled,
_ => false,
}
}
fn ensure_enabled() -> EvmResult<()> {
if is_enabled() {
Ok(())
} else {
Err(PrecompileFailure::Revert {
exit_status: ExitRevert::Reverted,
output: revert_as_bytes("GMP Precompile is not enabled"),
})
}
}
mod storage {
use super::*;
use frame_support::{
storage::types::{OptionQuery, StorageValue},
traits::StorageInstance,
};
pub struct CoreAddressStorageInstance;
impl StorageInstance for CoreAddressStorageInstance {
const STORAGE_PREFIX: &'static str = "CoreAddress";
fn pallet_prefix() -> &'static str {
"gmp"
}
}
pub type CoreAddress = StorageValue<CoreAddressStorageInstance, H160, OptionQuery>;
pub struct BridgeAddressStorageInstance;
impl StorageInstance for BridgeAddressStorageInstance {
const STORAGE_PREFIX: &'static str = "BridgeAddress";
fn pallet_prefix() -> &'static str {
"gmp"
}
}
pub type BridgeAddress = StorageValue<BridgeAddressStorageInstance, H160, OptionQuery>;
pub struct PrecompileEnabledStorageInstance;
impl StorageInstance for PrecompileEnabledStorageInstance {
const STORAGE_PREFIX: &'static str = "PrecompileEnabled";
fn pallet_prefix() -> &'static str {
"gmp"
}
}
pub type PrecompileEnabled = StorageValue<PrecompileEnabledStorageInstance, bool, OptionQuery>;
}