feat: initialize Kurdistan SDK - independent fork of Polkadot SDK
This commit is contained in:
@@ -0,0 +1,958 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts messages from Solidity ABI-encoding to XCM
|
||||
|
||||
use super::{message::*, traits::*};
|
||||
use crate::{v2::LOG_TARGET, CallIndex};
|
||||
use codec::{Decode, DecodeLimit, Encode};
|
||||
use core::marker::PhantomData;
|
||||
use frame_support::ensure;
|
||||
use snowbridge_core::{ParaId, TokenId};
|
||||
use sp_core::{Get, RuntimeDebug, H160};
|
||||
use sp_io::hashing::blake2_256;
|
||||
use sp_runtime::{traits::MaybeConvert, MultiAddress};
|
||||
use sp_std::prelude::*;
|
||||
use xcm::{
|
||||
prelude::{Junction::*, *},
|
||||
MAX_XCM_DECODE_DEPTH,
|
||||
};
|
||||
use xcm_builder::ExternalConsensusLocationsConverterFor;
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
/// Topic prefix used for generating unique identifiers for messages
|
||||
const INBOUND_QUEUE_TOPIC_PREFIX: &str = "SnowbridgeInboundQueueV2";
|
||||
|
||||
/// Representation of an intermediate parsed message, before final
|
||||
/// conversion to XCM.
|
||||
#[derive(Clone, RuntimeDebug, Encode)]
|
||||
pub struct PreparedMessage {
|
||||
/// Ethereum account that initiated this messaging operation
|
||||
pub origin: H160,
|
||||
/// The claimer in the case that funds get trapped.
|
||||
pub claimer: Location,
|
||||
/// The assets bridged from Ethereum
|
||||
pub assets: Vec<AssetTransfer>,
|
||||
/// The XCM to execute on the destination
|
||||
pub remote_xcm: Xcm<()>,
|
||||
/// Fee in Ether to cover the xcm execution on AH.
|
||||
pub execution_fee: Asset,
|
||||
}
|
||||
|
||||
/// An asset transfer instruction
|
||||
#[derive(Clone, RuntimeDebug, Encode)]
|
||||
pub enum AssetTransfer {
|
||||
ReserveDeposit(Asset),
|
||||
ReserveWithdraw(Asset),
|
||||
}
|
||||
|
||||
#[derive(Clone, RuntimeDebug, Encode)]
|
||||
pub struct CreateAssetCallInfo {
|
||||
pub create_call: CallIndex,
|
||||
pub deposit: u128,
|
||||
pub min_balance: u128,
|
||||
pub set_reserves_call: CallIndex,
|
||||
}
|
||||
|
||||
pub struct AssetHubUniversal<LocalNetwork, AssetHubParaId>(
|
||||
PhantomData<(LocalNetwork, AssetHubParaId)>,
|
||||
);
|
||||
impl<LocalNetwork, AssetHubParaId> Get<InteriorLocation>
|
||||
for AssetHubUniversal<LocalNetwork, AssetHubParaId>
|
||||
where
|
||||
LocalNetwork: Get<NetworkId>,
|
||||
AssetHubParaId: Get<ParaId>,
|
||||
{
|
||||
fn get() -> InteriorLocation {
|
||||
[GlobalConsensus(LocalNetwork::get()), Teyrchain(AssetHubParaId::get().into())].into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Concrete implementation of `ConvertMessage`
|
||||
pub struct MessageToXcm<
|
||||
CreateAssetCall,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayProxyAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
ConvertAssetId,
|
||||
AccountId,
|
||||
> {
|
||||
_phantom: PhantomData<(
|
||||
CreateAssetCall,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayProxyAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
ConvertAssetId,
|
||||
AccountId,
|
||||
)>,
|
||||
}
|
||||
|
||||
impl<
|
||||
CreateAssetCall,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayProxyAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
ConvertAssetId,
|
||||
AccountId,
|
||||
>
|
||||
MessageToXcm<
|
||||
CreateAssetCall,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayProxyAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
ConvertAssetId,
|
||||
AccountId,
|
||||
>
|
||||
where
|
||||
CreateAssetCall: Get<CreateAssetCallInfo>,
|
||||
EthereumNetwork: Get<NetworkId>,
|
||||
LocalNetwork: Get<NetworkId>,
|
||||
GatewayProxyAddress: Get<H160>,
|
||||
InboundQueueLocation: Get<InteriorLocation>,
|
||||
AssetHubParaId: Get<ParaId>,
|
||||
ConvertAssetId: MaybeConvert<TokenId, Location>,
|
||||
AccountId: Into<[u8; 32]> + From<[u8; 32]> + Clone,
|
||||
{
|
||||
/// Parse the message into an intermediate form, with all fields decoded
|
||||
/// and prepared.
|
||||
fn prepare(message: Message) -> Result<PreparedMessage, ConvertMessageError> {
|
||||
// ETH "asset id" is the Ethereum root location. Same location used for the "bridge owner".
|
||||
let ether_location = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]);
|
||||
let bridge_owner = Self::bridge_owner()?;
|
||||
|
||||
let claimer = message
|
||||
.claimer
|
||||
// Get the claimer from the message,
|
||||
.and_then(|claimer_bytes| Location::decode(&mut claimer_bytes.as_ref()).ok())
|
||||
// or use the Snowbridge sovereign on AH as the fallback claimer.
|
||||
.unwrap_or_else(|| {
|
||||
Location::new(0, [AccountId32 { network: None, id: bridge_owner.clone().into() }])
|
||||
});
|
||||
|
||||
let mut remote_xcm: Xcm<()> = match &message.xcm {
|
||||
XcmPayload::Raw(raw) => Self::decode_raw_xcm(raw),
|
||||
XcmPayload::CreateAsset { token, network } => Self::make_create_asset_xcm(
|
||||
token,
|
||||
*network,
|
||||
message.value,
|
||||
bridge_owner,
|
||||
claimer.clone(),
|
||||
)?,
|
||||
};
|
||||
|
||||
// Asset to cover XCM execution fee
|
||||
let execution_fee_asset: Asset = (ether_location.clone(), message.execution_fee).into();
|
||||
|
||||
let mut assets = vec![];
|
||||
|
||||
if message.value > 0 {
|
||||
// Asset for remaining ether
|
||||
let remaining_ether_asset: Asset = (ether_location.clone(), message.value).into();
|
||||
assets.push(AssetTransfer::ReserveDeposit(remaining_ether_asset));
|
||||
}
|
||||
|
||||
for asset in &message.assets {
|
||||
match asset {
|
||||
EthereumAsset::NativeTokenERC20 { token_id, value } => {
|
||||
ensure!(*token_id != H160::zero(), ConvertMessageError::InvalidAsset);
|
||||
let token_location: Location = Location::new(
|
||||
2,
|
||||
[
|
||||
GlobalConsensus(EthereumNetwork::get()),
|
||||
AccountKey20 { network: None, key: (*token_id).into() },
|
||||
],
|
||||
);
|
||||
let asset: Asset = (token_location, *value).into();
|
||||
assets.push(AssetTransfer::ReserveDeposit(asset));
|
||||
},
|
||||
EthereumAsset::ForeignTokenERC20 { token_id, value } => {
|
||||
let asset_location = ConvertAssetId::maybe_convert(*token_id)
|
||||
.ok_or(ConvertMessageError::InvalidAsset)?;
|
||||
let asset_hub_from_ethereum: Location = Location::new(
|
||||
1,
|
||||
[
|
||||
GlobalConsensus(LocalNetwork::get()),
|
||||
Teyrchain(AssetHubParaId::get().into()),
|
||||
],
|
||||
);
|
||||
let ethereum_universal: InteriorLocation =
|
||||
[GlobalConsensus(EthereumNetwork::get())].into();
|
||||
let reanchored_asset_location = asset_location
|
||||
.reanchored(&asset_hub_from_ethereum, ðereum_universal)
|
||||
.map_err(|_| ConvertMessageError::CannotReanchor)?;
|
||||
let asset: Asset = (reanchored_asset_location, *value).into();
|
||||
assets.push(AssetTransfer::ReserveWithdraw(asset));
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Add SetTopic instruction if not already present as the last instruction
|
||||
if !matches!(remote_xcm.0.last(), Some(SetTopic(_))) {
|
||||
let topic = blake2_256(&(INBOUND_QUEUE_TOPIC_PREFIX, message.nonce).encode());
|
||||
remote_xcm.0.push(SetTopic(topic));
|
||||
}
|
||||
|
||||
let prepared_message = PreparedMessage {
|
||||
origin: message.origin,
|
||||
claimer,
|
||||
assets,
|
||||
remote_xcm,
|
||||
execution_fee: execution_fee_asset,
|
||||
};
|
||||
|
||||
Ok(prepared_message)
|
||||
}
|
||||
|
||||
/// Get sovereign account of Ethereum on Asset Hub.
|
||||
fn bridge_owner() -> Result<AccountId, ConvertMessageError> {
|
||||
let account =
|
||||
ExternalConsensusLocationsConverterFor::<
|
||||
AssetHubUniversal<LocalNetwork, AssetHubParaId>,
|
||||
AccountId,
|
||||
>::convert_location(&Location::new(2, [GlobalConsensus(EthereumNetwork::get())]))
|
||||
.ok_or(ConvertMessageError::CannotReanchor)?;
|
||||
|
||||
Ok(account)
|
||||
}
|
||||
|
||||
/// Construct the remote XCM needed to create a new asset in the `ForeignAssets` pallet
|
||||
/// on AssetHub. Pezkuwi is the only supported network at the moment.
|
||||
fn make_create_asset_xcm(
|
||||
token: &H160,
|
||||
network: super::message::Network,
|
||||
eth_value: u128,
|
||||
bridge_owner: AccountId,
|
||||
claimer: Location,
|
||||
) -> Result<Xcm<()>, ConvertMessageError> {
|
||||
let dot_asset = Location::new(1, Here);
|
||||
let dot_fee: xcm::prelude::Asset = (dot_asset, CreateAssetCall::get().deposit).into();
|
||||
|
||||
let eth_asset: xcm::prelude::Asset =
|
||||
(Location::new(2, [GlobalConsensus(EthereumNetwork::get())]), eth_value).into();
|
||||
|
||||
let create_call_index: [u8; 2] = CreateAssetCall::get().create_call;
|
||||
let create_min_blance: u128 = CreateAssetCall::get().min_balance;
|
||||
let set_reserves_call_index: [u8; 2] = CreateAssetCall::get().set_reserves_call;
|
||||
|
||||
let asset_id = Location::new(
|
||||
2,
|
||||
[
|
||||
GlobalConsensus(EthereumNetwork::get()),
|
||||
AccountKey20 { network: None, key: (*token).into() },
|
||||
],
|
||||
);
|
||||
|
||||
match network {
|
||||
super::message::Network::Pezkuwi => Ok(Self::make_create_asset_xcm_for_pezkuwi(
|
||||
create_call_index,
|
||||
set_reserves_call_index,
|
||||
create_min_blance,
|
||||
asset_id,
|
||||
bridge_owner,
|
||||
dot_fee,
|
||||
eth_asset,
|
||||
claimer,
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct the asset creation XCM for the Polkdot network.
|
||||
fn make_create_asset_xcm_for_pezkuwi(
|
||||
create_call_index: [u8; 2],
|
||||
set_reserves_call_index: [u8; 2],
|
||||
create_min_blance: u128,
|
||||
asset_id: Location,
|
||||
bridge_owner: AccountId,
|
||||
dot_fee_asset: xcm::prelude::Asset,
|
||||
eth_asset: xcm::prelude::Asset,
|
||||
claimer: Location,
|
||||
) -> Xcm<()> {
|
||||
let bridge_owner_bytes: [u8; 32] = bridge_owner.into();
|
||||
let reserve_data = assets_common::local_and_foreign_assets::ForeignAssetReserveData {
|
||||
reserve: Location::new(2, [GlobalConsensus(EthereumNetwork::get())]),
|
||||
teleportable: false,
|
||||
};
|
||||
vec![
|
||||
// Exchange eth for dot to pay the asset creation deposit.
|
||||
ExchangeAsset {
|
||||
give: eth_asset.into(),
|
||||
want: dot_fee_asset.clone().into(),
|
||||
maximal: false,
|
||||
},
|
||||
// Deposit the dot deposit into the bridge sovereign account (where the asset
|
||||
// creation fee will be deducted from).
|
||||
DepositAsset {
|
||||
assets: dot_fee_asset.clone().into(),
|
||||
beneficiary: bridge_owner_bytes.into(),
|
||||
},
|
||||
// Call to create the asset.
|
||||
Transact {
|
||||
origin_kind: OriginKind::Xcm,
|
||||
fallback_max_weight: None,
|
||||
call: (
|
||||
create_call_index,
|
||||
asset_id.clone(),
|
||||
MultiAddress::<[u8; 32], ()>::Id(bridge_owner_bytes.into()),
|
||||
create_min_blance,
|
||||
)
|
||||
.encode()
|
||||
.into(),
|
||||
},
|
||||
// Call to set Ethereum as the asset's reserve.
|
||||
Transact {
|
||||
origin_kind: OriginKind::Xcm,
|
||||
fallback_max_weight: None,
|
||||
call: (set_reserves_call_index, asset_id, vec![reserve_data]).encode().into(),
|
||||
},
|
||||
RefundSurplus,
|
||||
// Deposit leftover funds to Snowbridge sovereign
|
||||
DepositAsset { assets: Wild(AllCounted(2)), beneficiary: claimer },
|
||||
]
|
||||
.into()
|
||||
}
|
||||
|
||||
/// Parse and (non-strictly) decode `raw` XCM bytes into a `Xcm<()>`.
|
||||
/// If decoding fails, return an empty `Xcm<()>`—thus allowing the message
|
||||
/// to proceed so assets can still be trapped on AH rather than the funds being locked on
|
||||
/// Ethereum but not accessible on AH.
|
||||
fn decode_raw_xcm(raw: &[u8]) -> Xcm<()> {
|
||||
let mut data = raw;
|
||||
if let Ok(versioned_xcm) =
|
||||
VersionedXcm::<()>::decode_with_depth_limit(MAX_XCM_DECODE_DEPTH, &mut data)
|
||||
{
|
||||
if let Ok(decoded_xcm) = versioned_xcm.try_into() {
|
||||
return decoded_xcm;
|
||||
}
|
||||
}
|
||||
// Decoding failed; allow an empty XCM so the message won't fail entirely.
|
||||
Xcm::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<
|
||||
CreateAsset,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayProxyAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
ConvertAssetId,
|
||||
AccountId,
|
||||
> ConvertMessage
|
||||
for MessageToXcm<
|
||||
CreateAsset,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayProxyAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
ConvertAssetId,
|
||||
AccountId,
|
||||
>
|
||||
where
|
||||
CreateAsset: Get<CreateAssetCallInfo>,
|
||||
EthereumNetwork: Get<NetworkId>,
|
||||
LocalNetwork: Get<NetworkId>,
|
||||
GatewayProxyAddress: Get<H160>,
|
||||
InboundQueueLocation: Get<InteriorLocation>,
|
||||
AssetHubParaId: Get<ParaId>,
|
||||
ConvertAssetId: MaybeConvert<TokenId, Location>,
|
||||
AccountId: Into<[u8; 32]> + From<[u8; 32]> + Clone,
|
||||
{
|
||||
fn convert(message: Message) -> Result<Xcm<()>, ConvertMessageError> {
|
||||
let message = Self::prepare(message)?;
|
||||
|
||||
tracing::trace!(target: LOG_TARGET, ?message, "prepared message");
|
||||
|
||||
let mut instructions = vec![
|
||||
DescendOrigin(InboundQueueLocation::get()),
|
||||
UniversalOrigin(GlobalConsensus(EthereumNetwork::get())),
|
||||
ReserveAssetDeposited(message.execution_fee.clone().into()),
|
||||
];
|
||||
|
||||
// Set claimer before PayFees, in case the fees are not enough. Then the claimer will be
|
||||
// able to claim the funds still.
|
||||
instructions.push(SetHints {
|
||||
hints: vec![AssetClaimer { location: message.claimer }]
|
||||
.try_into()
|
||||
.expect("checked statically, qed"),
|
||||
});
|
||||
|
||||
instructions.push(PayFees { asset: message.execution_fee.clone() });
|
||||
|
||||
let mut reserve_deposit_assets = vec![];
|
||||
let mut reserve_withdraw_assets = vec![];
|
||||
|
||||
for asset in message.assets {
|
||||
match asset {
|
||||
AssetTransfer::ReserveDeposit(asset) => reserve_deposit_assets.push(asset),
|
||||
AssetTransfer::ReserveWithdraw(asset) => reserve_withdraw_assets.push(asset),
|
||||
};
|
||||
}
|
||||
|
||||
if !reserve_deposit_assets.is_empty() {
|
||||
instructions.push(ReserveAssetDeposited(reserve_deposit_assets.into()));
|
||||
}
|
||||
if !reserve_withdraw_assets.is_empty() {
|
||||
instructions.push(WithdrawAsset(reserve_withdraw_assets.into()));
|
||||
}
|
||||
|
||||
// If the message origin is not the gateway proxy contract, set the origin to
|
||||
// the original sender on Ethereum. Important to be before the arbitrary XCM that is
|
||||
// appended to the message on the next line.
|
||||
if message.origin != GatewayProxyAddress::get() {
|
||||
instructions.push(DescendOrigin(
|
||||
AccountKey20 { key: message.origin.into(), network: None }.into(),
|
||||
));
|
||||
}
|
||||
|
||||
// Add the XCM sent in the message to the end of the xcm instruction
|
||||
instructions.extend(message.remote_xcm.0);
|
||||
|
||||
Ok(instructions.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use codec::Encode;
|
||||
use frame_support::{assert_err, assert_ok, parameter_types};
|
||||
use hex_literal::hex;
|
||||
use snowbridge_core::TokenId;
|
||||
use snowbridge_test_utils::mock_converter::{
|
||||
add_location_override, reanchor_to_ethereum, LocationIdConvert,
|
||||
};
|
||||
use sp_core::{H160, H256};
|
||||
const GATEWAY_ADDRESS: [u8; 20] = hex!["eda338e4dc46038493b885327842fd3e301cab39"];
|
||||
|
||||
parameter_types! {
|
||||
pub const EthereumNetwork: NetworkId = NetworkId::Ethereum { chain_id: 1 };
|
||||
pub const LocalNetwork: NetworkId = NetworkId::Pezkuwi;
|
||||
pub const GatewayAddress: H160 = H160(GATEWAY_ADDRESS);
|
||||
pub InboundQueueLocation: InteriorLocation = [PalletInstance(84)].into();
|
||||
pub EthereumUniversalLocation: InteriorLocation =
|
||||
[GlobalConsensus(EthereumNetwork::get())].into();
|
||||
pub AssetHubParaId: ParaId = 1000.into();
|
||||
pub const CreateAssetCallIndex: [u8;2] = [53, 0];
|
||||
pub const SetReservesCallIndex: [u8;2] = [53, 33];
|
||||
pub const CreateAssetDeposit: u128 = 10_000_000_000u128;
|
||||
pub const CreateAssetMinBalance: u128 = 1;
|
||||
pub EthereumLocation: Location = Location::new(2,EthereumUniversalLocation::get());
|
||||
pub BridgeHubContext: InteriorLocation = [GlobalConsensus(Pezkuwi),Teyrchain(1002)].into();
|
||||
pub CreateAssetCall: CreateAssetCallInfo = CreateAssetCallInfo {
|
||||
create_call: CreateAssetCallIndex::get(),
|
||||
deposit: CreateAssetDeposit::get(),
|
||||
min_balance: CreateAssetMinBalance::get(),
|
||||
set_reserves_call: SetReservesCallIndex::get(),
|
||||
};
|
||||
}
|
||||
|
||||
pub struct MockFailedTokenConvert;
|
||||
impl MaybeConvert<TokenId, Location> for MockFailedTokenConvert {
|
||||
fn maybe_convert(_id: TokenId) -> Option<Location> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
type Converter = MessageToXcm<
|
||||
CreateAssetCall,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
LocationIdConvert,
|
||||
[u8; 32],
|
||||
>;
|
||||
|
||||
type ConverterFailing = MessageToXcm<
|
||||
CreateAssetCall,
|
||||
EthereumNetwork,
|
||||
LocalNetwork,
|
||||
GatewayAddress,
|
||||
InboundQueueLocation,
|
||||
AssetHubParaId,
|
||||
MockFailedTokenConvert,
|
||||
[u8; 32],
|
||||
>;
|
||||
|
||||
#[test]
|
||||
fn test_successful_message() {
|
||||
sp_io::TestExternalities::default().execute_with(|| {
|
||||
let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
|
||||
let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into();
|
||||
let dot_location = Location::parent();
|
||||
let (foreign_token_id, _) = reanchor_to_ethereum(
|
||||
dot_location.clone(),
|
||||
EthereumLocation::get(),
|
||||
BridgeHubContext::get(),
|
||||
);
|
||||
add_location_override(dot_location, EthereumLocation::get(), BridgeHubContext::get());
|
||||
let beneficiary: Location =
|
||||
hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into();
|
||||
let token_value = 3_000_000_000_000u128;
|
||||
let assets = vec![
|
||||
EthereumAsset::NativeTokenERC20 { token_id: native_token_id, value: token_value },
|
||||
EthereumAsset::ForeignTokenERC20 { token_id: foreign_token_id, value: token_value },
|
||||
];
|
||||
let instructions = vec![DepositAsset {
|
||||
assets: Wild(AllCounted(1).into()),
|
||||
beneficiary: beneficiary.clone(),
|
||||
}];
|
||||
let xcm: Xcm<()> = instructions.into();
|
||||
let versioned_xcm = VersionedXcm::V5(xcm);
|
||||
let claimer_location =
|
||||
Location::new(0, AccountId32 { network: None, id: H256::random().into() });
|
||||
let claimer: Option<Vec<u8>> = Some(claimer_location.clone().encode());
|
||||
let value = 6_000_000_000_000u128;
|
||||
let execution_fee = 1_000_000_000_000u128;
|
||||
let relayer_fee = 5_000_000_000_000u128;
|
||||
|
||||
let message = Message {
|
||||
gateway: H160::zero(),
|
||||
nonce: 0,
|
||||
origin,
|
||||
assets,
|
||||
xcm: XcmPayload::Raw(versioned_xcm.encode()),
|
||||
claimer,
|
||||
value,
|
||||
execution_fee,
|
||||
relayer_fee,
|
||||
};
|
||||
|
||||
let result = Converter::convert(message);
|
||||
|
||||
assert_ok!(result.clone());
|
||||
|
||||
let xcm = result.unwrap();
|
||||
|
||||
// Convert to vec for easier inspection
|
||||
let instructions: Vec<_> = xcm.into_iter().collect();
|
||||
|
||||
// Check last instruction is a SetTopic (automatically added)
|
||||
let last_instruction =
|
||||
instructions.last().expect("should have at least one instruction");
|
||||
assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic");
|
||||
|
||||
let mut asset_claimer_found = false;
|
||||
let mut pay_fees_found = false;
|
||||
let mut descend_origin_found = 0;
|
||||
let mut reserve_deposited_found = 0;
|
||||
let mut withdraw_assets_found = 0;
|
||||
let mut deposit_asset_found = 0;
|
||||
|
||||
for instruction in &instructions {
|
||||
if let SetHints { ref hints } = instruction {
|
||||
if let Some(AssetClaimer { ref location }) = hints.clone().into_iter().next() {
|
||||
assert_eq!(claimer_location, location.clone());
|
||||
asset_claimer_found = true;
|
||||
}
|
||||
}
|
||||
if let DescendOrigin(ref location) = instruction {
|
||||
descend_origin_found += 1;
|
||||
// The second DescendOrigin should be the message.origin (sender)
|
||||
if descend_origin_found == 2 {
|
||||
let junctions: Junctions =
|
||||
AccountKey20 { key: origin.into(), network: None }.into();
|
||||
assert_eq!(junctions, location.clone());
|
||||
}
|
||||
}
|
||||
if let PayFees { ref asset } = instruction {
|
||||
let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]);
|
||||
assert_eq!(asset.id, AssetId(fee_asset));
|
||||
assert_eq!(asset.fun, Fungible(execution_fee));
|
||||
pay_fees_found = true;
|
||||
}
|
||||
if let ReserveAssetDeposited(ref reserve_assets) = instruction {
|
||||
reserve_deposited_found += 1;
|
||||
if reserve_deposited_found == 1 {
|
||||
let fee_asset = Location::new(2, [GlobalConsensus(EthereumNetwork::get())]);
|
||||
let fee: Asset = (fee_asset, execution_fee).into();
|
||||
let fee_assets: Assets = fee.into();
|
||||
assert_eq!(fee_assets, reserve_assets.clone());
|
||||
}
|
||||
if reserve_deposited_found == 2 {
|
||||
let token_asset = Location::new(
|
||||
2,
|
||||
[
|
||||
GlobalConsensus(EthereumNetwork::get()),
|
||||
AccountKey20 { network: None, key: native_token_id.into() },
|
||||
],
|
||||
);
|
||||
let token: Asset = (token_asset, token_value).into();
|
||||
|
||||
let remaining_ether_asset: Asset =
|
||||
(Location::new(2, [GlobalConsensus(EthereumNetwork::get())]), value)
|
||||
.into();
|
||||
|
||||
let expected_assets: Assets = vec![token, remaining_ether_asset].into();
|
||||
assert_eq!(expected_assets, reserve_assets.clone());
|
||||
}
|
||||
}
|
||||
if let WithdrawAsset(ref withdraw_assets) = instruction {
|
||||
withdraw_assets_found += 1;
|
||||
let token_asset = Location::new(1, Here);
|
||||
let token: Asset = (token_asset, token_value).into();
|
||||
let token_assets: Assets = token.into();
|
||||
assert_eq!(token_assets, withdraw_assets.clone());
|
||||
}
|
||||
if let DepositAsset { ref assets, beneficiary: deposit_beneficiary } = instruction {
|
||||
deposit_asset_found += 1;
|
||||
if deposit_asset_found == 1 {
|
||||
assert_eq!(AssetFilter::from(Wild(AllCounted(1).into())), assets.clone());
|
||||
assert_eq!(*deposit_beneficiary, beneficiary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetAssetClaimer must be in the message.
|
||||
assert!(asset_claimer_found);
|
||||
// PayFees must be in the message.
|
||||
assert!(pay_fees_found);
|
||||
// The first DescendOrigin to descend into the InboundV2 pallet index and the
|
||||
// DescendOrigin into the message.origin
|
||||
assert!(descend_origin_found == 2);
|
||||
// Expecting two ReserveAssetDeposited instructions, one for the fee and one for the
|
||||
// token being transferred.
|
||||
assert!(reserve_deposited_found == 2);
|
||||
// Expecting one WithdrawAsset for the foreign ERC-20
|
||||
assert!(withdraw_assets_found == 1);
|
||||
// Deposit asset added by user
|
||||
assert!(deposit_asset_found == 1);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_message_with_gateway_origin_does_not_descend_origin_into_sender() {
|
||||
let origin: H160 = GatewayAddress::get();
|
||||
let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into();
|
||||
let beneficiary =
|
||||
hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into();
|
||||
let message_id: H256 =
|
||||
hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into();
|
||||
let token_value = 3_000_000_000_000u128;
|
||||
let assets =
|
||||
vec![EthereumAsset::NativeTokenERC20 { token_id: native_token_id, value: token_value }];
|
||||
let instructions = vec![
|
||||
DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary },
|
||||
SetTopic(message_id.into()),
|
||||
];
|
||||
let xcm: Xcm<()> = instructions.into();
|
||||
let versioned_xcm = VersionedXcm::V5(xcm);
|
||||
let claimer_account = AccountId32 { network: None, id: H256::random().into() };
|
||||
let claimer: Option<Vec<u8>> = Some(claimer_account.clone().encode());
|
||||
let value = 6_000_000_000_000u128;
|
||||
let execution_fee = 1_000_000_000_000u128;
|
||||
let relayer_fee = 5_000_000_000_000u128;
|
||||
|
||||
let message = Message {
|
||||
gateway: H160::zero(),
|
||||
nonce: 0,
|
||||
origin,
|
||||
assets,
|
||||
xcm: XcmPayload::Raw(versioned_xcm.encode()),
|
||||
claimer,
|
||||
value,
|
||||
execution_fee,
|
||||
relayer_fee,
|
||||
};
|
||||
|
||||
let result = Converter::convert(message);
|
||||
|
||||
assert_ok!(result.clone());
|
||||
|
||||
let xcm = result.unwrap();
|
||||
|
||||
let mut instructions = xcm.into_iter();
|
||||
let mut commands_found = 0;
|
||||
while let Some(instruction) = instructions.next() {
|
||||
if let DescendOrigin(ref _location) = instruction {
|
||||
commands_found = commands_found + 1;
|
||||
}
|
||||
}
|
||||
// There should only be 1 DescendOrigin in the message.
|
||||
assert!(commands_found == 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_foreign_erc20() {
|
||||
let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
|
||||
let token_id: H256 =
|
||||
hex!("37a6c666da38711a963d938eafdd09314fd3f95a96a3baffb55f26560f4ecdd8").into();
|
||||
let beneficiary =
|
||||
hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into();
|
||||
let message_id: H256 =
|
||||
hex!("8b69c7e376e28114618e829a7ec768dbda28357d359ba417a3bd79b11215059d").into();
|
||||
let token_value = 3_000_000_000_000u128;
|
||||
let assets = vec![EthereumAsset::ForeignTokenERC20 { token_id, value: token_value }];
|
||||
let instructions = vec![
|
||||
DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary },
|
||||
SetTopic(message_id.into()),
|
||||
];
|
||||
let xcm: Xcm<()> = instructions.into();
|
||||
let versioned_xcm = VersionedXcm::V5(xcm);
|
||||
let claimer_account = AccountId32 { network: None, id: H256::random().into() };
|
||||
let claimer: Option<Vec<u8>> = Some(claimer_account.clone().encode());
|
||||
let value = 0;
|
||||
let execution_fee = 1_000_000_000_000u128;
|
||||
let relayer_fee = 5_000_000_000_000u128;
|
||||
|
||||
let message = Message {
|
||||
gateway: H160::zero(),
|
||||
nonce: 0,
|
||||
origin,
|
||||
assets,
|
||||
xcm: XcmPayload::Raw(versioned_xcm.encode()),
|
||||
claimer,
|
||||
value,
|
||||
execution_fee,
|
||||
relayer_fee,
|
||||
};
|
||||
|
||||
assert_err!(ConverterFailing::convert(message), ConvertMessageError::InvalidAsset);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_claimer() {
|
||||
sp_io::TestExternalities::default().execute_with(|| {
|
||||
let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
|
||||
let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into();
|
||||
let beneficiary =
|
||||
hex!("908783d8cd24c9e02cee1d26ab9c46d458621ad0150b626c536a40b9df3f09c6").into();
|
||||
let token_value = 3_000_000_000_000u128;
|
||||
let assets = vec![EthereumAsset::NativeTokenERC20 {
|
||||
token_id: native_token_id,
|
||||
value: token_value,
|
||||
}];
|
||||
let instructions =
|
||||
vec![DepositAsset { assets: Wild(AllCounted(1).into()), beneficiary }];
|
||||
let xcm: Xcm<()> = instructions.into();
|
||||
let versioned_xcm = VersionedXcm::V5(xcm);
|
||||
// Invalid claimer location, cannot be decoded into a Location
|
||||
let claimer: Option<Vec<u8>> = Some(vec![]);
|
||||
let value = 6_000_000_000_000u128;
|
||||
let execution_fee = 1_000_000_000_000u128;
|
||||
let relayer_fee = 5_000_000_000_000u128;
|
||||
|
||||
let message = Message {
|
||||
gateway: H160::zero(),
|
||||
nonce: 0,
|
||||
origin,
|
||||
assets,
|
||||
xcm: XcmPayload::Raw(versioned_xcm.encode()),
|
||||
claimer,
|
||||
value,
|
||||
execution_fee,
|
||||
relayer_fee,
|
||||
};
|
||||
|
||||
let result = Converter::convert(message.clone());
|
||||
|
||||
// Invalid claimer does not break the message conversion
|
||||
assert_ok!(result.clone());
|
||||
|
||||
let xcm = result.unwrap();
|
||||
let instructions: Vec<_> = xcm.into_iter().collect();
|
||||
|
||||
// Check last instruction is a SetTopic (automatically added)
|
||||
let last_instruction =
|
||||
instructions.last().expect("should have at least one instruction");
|
||||
assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic");
|
||||
|
||||
let mut actual_claimer: Option<Location> = None;
|
||||
for instruction in &instructions {
|
||||
if let SetHints { ref hints } = instruction {
|
||||
if let Some(AssetClaimer { location }) = hints.clone().into_iter().next() {
|
||||
actual_claimer = Some(location);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// actual claimer should default to Snowbridge sovereign account
|
||||
let bridge_owner = ExternalConsensusLocationsConverterFor::<
|
||||
AssetHubUniversal<LocalNetwork, AssetHubParaId>,
|
||||
[u8; 32],
|
||||
>::convert_location(&Location::new(
|
||||
2,
|
||||
[GlobalConsensus(EthereumNetwork::get())],
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
actual_claimer,
|
||||
Some(Location::new(0, [AccountId32 { network: None, id: bridge_owner }]))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_xcm() {
|
||||
sp_io::TestExternalities::default().execute_with(|| {
|
||||
let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
|
||||
let native_token_id: H160 = hex!("5615deb798bb3e4dfa0139dfa1b3d433cc23b72f").into();
|
||||
let token_value = 3_000_000_000_000u128;
|
||||
let assets = vec![EthereumAsset::NativeTokenERC20 {
|
||||
token_id: native_token_id,
|
||||
value: token_value,
|
||||
}];
|
||||
// invalid xcm
|
||||
let versioned_xcm = hex!("8b69c7e376e28114618e829a7ec7").to_vec();
|
||||
let claimer_account = AccountId32 { network: None, id: H256::random().into() };
|
||||
let claimer: Option<Vec<u8>> = Some(claimer_account.clone().encode());
|
||||
let value = 6_000_000_000_000u128;
|
||||
let execution_fee = 1_000_000_000_000u128;
|
||||
let relayer_fee = 5_000_000_000_000u128;
|
||||
|
||||
let message = Message {
|
||||
gateway: H160::zero(),
|
||||
nonce: 0,
|
||||
origin,
|
||||
assets,
|
||||
xcm: XcmPayload::Raw(versioned_xcm),
|
||||
claimer: Some(claimer.encode()),
|
||||
value,
|
||||
execution_fee,
|
||||
relayer_fee,
|
||||
};
|
||||
|
||||
let result = Converter::convert(message);
|
||||
|
||||
// Invalid xcm does not break the message, allowing funds to be trapped on AH.
|
||||
assert_ok!(result.clone());
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_with_set_topic_respects_user_topic() {
|
||||
sp_io::TestExternalities::default().execute_with(|| {
|
||||
let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
|
||||
|
||||
// Create a custom topic ID that the user specifies
|
||||
let user_topic: [u8; 32] =
|
||||
hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
|
||||
|
||||
// User's XCM with a SetTopic as the last instruction
|
||||
let instructions = vec![RefundSurplus, SetTopic(user_topic)];
|
||||
let xcm: Xcm<()> = instructions.into();
|
||||
let versioned_xcm = VersionedXcm::V5(xcm);
|
||||
|
||||
let execution_fee = 1_000_000_000_000u128;
|
||||
let value = 0;
|
||||
|
||||
let message = Message {
|
||||
gateway: H160::zero(),
|
||||
nonce: 0,
|
||||
origin,
|
||||
assets: vec![],
|
||||
xcm: XcmPayload::Raw(versioned_xcm.encode()),
|
||||
claimer: None,
|
||||
value,
|
||||
execution_fee,
|
||||
relayer_fee: 0,
|
||||
};
|
||||
|
||||
let result = Converter::convert(message);
|
||||
assert_ok!(result.clone());
|
||||
|
||||
let xcm = result.unwrap();
|
||||
let instructions: Vec<_> = xcm.into_iter().collect();
|
||||
|
||||
// The last instruction should be the user's SetTopic
|
||||
let last_instruction =
|
||||
instructions.last().expect("should have at least one instruction");
|
||||
if let SetTopic(ref topic) = last_instruction {
|
||||
assert_eq!(*topic, user_topic);
|
||||
} else {
|
||||
panic!("Last instruction should be SetTopic");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_with_generates_a_unique_topic_if_no_topic_is_present() {
|
||||
sp_io::TestExternalities::default().execute_with(|| {
|
||||
let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
|
||||
|
||||
let execution_fee = 1_000_000_000_000u128;
|
||||
let value = 0;
|
||||
|
||||
let message = Message {
|
||||
gateway: H160::zero(),
|
||||
nonce: 0,
|
||||
origin,
|
||||
assets: vec![],
|
||||
xcm: XcmPayload::Raw(vec![]),
|
||||
claimer: None,
|
||||
value,
|
||||
execution_fee,
|
||||
relayer_fee: 0,
|
||||
};
|
||||
|
||||
let result = Converter::convert(message);
|
||||
assert_ok!(result.clone());
|
||||
|
||||
let xcm = result.unwrap();
|
||||
let instructions: Vec<_> = xcm.into_iter().collect();
|
||||
|
||||
// The last instruction should be a SetTopic
|
||||
let last_instruction =
|
||||
instructions.last().expect("should have at least one instruction");
|
||||
assert!(matches!(last_instruction, SetTopic(_)));
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn message_with_user_topic_not_last_instruction_gets_appended() {
|
||||
sp_io::TestExternalities::default().execute_with(|| {
|
||||
let origin: H160 = hex!("29e3b139f4393adda86303fcdaa35f60bb7092bf").into();
|
||||
|
||||
let execution_fee = 1_000_000_000_000u128;
|
||||
let value = 0;
|
||||
|
||||
let user_topic: [u8; 32] =
|
||||
hex!("1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef");
|
||||
|
||||
// Add a set topic, but not as the last instruction.
|
||||
let instructions = vec![SetTopic(user_topic), RefundSurplus];
|
||||
let xcm: Xcm<()> = instructions.into();
|
||||
let versioned_xcm = VersionedXcm::V5(xcm);
|
||||
|
||||
let message = Message {
|
||||
gateway: H160::zero(),
|
||||
nonce: 0,
|
||||
origin,
|
||||
assets: vec![],
|
||||
xcm: XcmPayload::Raw(versioned_xcm.encode()),
|
||||
claimer: None,
|
||||
value,
|
||||
execution_fee,
|
||||
relayer_fee: 0,
|
||||
};
|
||||
|
||||
let result = Converter::convert(message);
|
||||
assert_ok!(result.clone());
|
||||
|
||||
let xcm = result.unwrap();
|
||||
let instructions: Vec<_> = xcm.into_iter().collect();
|
||||
|
||||
// Get the last instruction - should still be a SetTopic, but might not have the
|
||||
// original topic since for non-last-instruction topics, the filter_topic function
|
||||
// extracts it during prepare() and then the original value is later lost when we
|
||||
// append a new one
|
||||
let last_instruction =
|
||||
instructions.last().expect("should have at least one instruction");
|
||||
|
||||
// Check if the last instruction is a SetTopic (content isn't important)
|
||||
assert!(matches!(last_instruction, SetTopic(_)), "Last instruction should be SetTopic");
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user