Skip to content
This repository has been archived by the owner on Oct 22, 2024. It is now read-only.

Commit

Permalink
Multi hop transfer
Browse files Browse the repository at this point in the history
  • Loading branch information
yrong committed Aug 27, 2024
1 parent 4a04f1f commit 0d3988c
Show file tree
Hide file tree
Showing 5 changed files with 210 additions and 43 deletions.
82 changes: 63 additions & 19 deletions bridges/snowbridge/primitives/router/src/inbound/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ pub enum Command {
destination: Destination,
/// Amount to transfer
amount: u128,
/// XCM execution fee on AssetHub
fee: u128,
},
}

Expand Down Expand Up @@ -180,9 +182,15 @@ where
Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee)),
V1(MessageV1 {
chain_id,
command: SendNativeToken { token_id, destination, amount },
}) =>
Self::convert_send_native_token(message_id, chain_id, token_id, destination, amount),
command: SendNativeToken { token_id, destination, amount, fee },
}) => Self::convert_send_native_token(
message_id,
chain_id,
token_id,
destination,
amount,
fee,
),
}
}
}
Expand Down Expand Up @@ -363,21 +371,25 @@ where
token_id: TokenId,
destination: Destination,
amount: u128,
asset_hub_fee: u128,
) -> Result<(Xcm<()>, Balance), ConvertMessageError> {
let network = Ethereum { chain_id };
let asset_hub_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();

let (beneficiary, destination_fee) = match destination {
let (dest_para_id, beneficiary, dest_para_fee) = match destination {
// Final destination is a 32-byte account on AssetHub
Destination::AccountId32 { id } =>
(None, Location::new(0, [AccountId32 { network: None, id }]), 0),
// Final destination is a 32-byte account on a sibling of AssetHub
Destination::ForeignAccountId32 { id, fee, .. } =>
Some((Location::new(0, [AccountId32 { network: None, id }]), fee)),
Destination::ForeignAccountId32 { para_id, id, fee } =>
(Some(para_id), Location::new(0, [AccountId32 { network: None, id }]), fee),
// Final destination is a 20-byte account on a sibling of AssetHub
Destination::ForeignAccountId20 { id, fee, .. } =>
Some((Location::new(0, [AccountKey20 { network: None, key: id }]), fee)),
_ => None,
}
.ok_or(ConvertMessageError::InvalidDestination)?;
Destination::ForeignAccountId20 { para_id, id, fee } =>
(Some(para_id), Location::new(0, [AccountKey20 { network: None, key: id }]), fee),
};

let fee_asset: Asset = (Location::parent(), destination_fee).into();
let total_fees = asset_hub_fee.saturating_add(dest_para_fee);
let total_fee_asset: Asset = (Location::parent(), total_fees).into();

let versioned_asset_id =
ConvertAssetId::convert(&token_id).ok_or(ConvertMessageError::InvalidToken)?;
Expand All @@ -389,18 +401,50 @@ where

let inbound_queue_pallet_index = InboundQueuePalletInstance::get();

let instructions = vec![
ReceiveTeleportedAsset(fee_asset.clone().into()),
BuyExecution { fees: fee_asset, weight_limit: Unlimited },
let mut instructions = vec![
ReceiveTeleportedAsset(total_fee_asset.clone().into()),
BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
UniversalOrigin(GlobalConsensus(network)),
WithdrawAsset(asset.clone().into()),
ClearOrigin,
DepositAsset { assets: AllCounted(2).into(), beneficiary },
SetTopic(message_id.into()),
SetFeesMode { jit_withdraw: true },
];

Ok((instructions.into(), destination_fee.into()))
match dest_para_id {
Some(dest_para_id) => {
let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into();

instructions.extend(vec![
// Perform a deposit reserve to send to destination chain.
InitiateReserveWithdraw {
assets: Wild(AllCounted(2)),
reserve: Location::new(1, [Parachain(dest_para_id)]),
xcm: vec![
// Buy execution on target.
BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
// Deposit asset to beneficiary.
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
// Forward message id to destination parachain.
SetTopic(message_id.into()),
]
.into(),
},
]);
},
None => {
instructions.extend(vec![
// Deposit both asset and fees to beneficiary so the fees will not get
// trapped. Another benefit is when fees left more than ED on AssetHub could be
// used to create the beneficiary account in case it does not exist.
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
]);
},
}

// Forward message id to Asset Hub.
instructions.push(SetTopic(message_id.into()));

Ok((instructions.into(), total_fees.into()))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ use sp_core::{sr25519, storage::Storage};
// Cumulus
use emulated_integration_tests_common::{
accounts, build_genesis_storage, collators, get_account_id_from_seed,
PenpalSiblingSovereignAccount, PenpalTeleportableAssetLocation, RESERVABLE_ASSET_ID,
PenpalBSiblingSovereignAccount, PenpalBTeleportableAssetLocation, RESERVABLE_ASSET_ID,
SAFE_XCM_VERSION,
};
use parachains_common::{AccountId, Balance};
Expand Down Expand Up @@ -71,8 +71,8 @@ pub fn genesis() -> Storage {
assets: vec![
// Penpal's teleportable asset representation
(
PenpalTeleportableAssetLocation::get(),
PenpalSiblingSovereignAccount::get(),
PenpalBTeleportableAssetLocation::get(),
PenpalBSiblingSovereignAccount::get(),
true,
ED,
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub const TELEPORTABLE_ASSET_ID: u32 = 2;
pub const USDT_ID: u32 = 1984;

pub const PENPAL_ID: u32 = 2000;
pub const PENPAL_B_ID: u32 = 2001;
pub const ASSETS_PALLET_ID: u8 = 50;

parameter_types! {
Expand All @@ -71,6 +72,14 @@ parameter_types! {
]
);
pub PenpalSiblingSovereignAccount: AccountId = Sibling::from(PENPAL_ID).into_account_truncating();
pub PenpalBTeleportableAssetLocation: xcm::v3::Location
= xcm::v3::Location::new(1, [
xcm::v3::Junction::Parachain(PENPAL_B_ID),
xcm::v3::Junction::PalletInstance(ASSETS_PALLET_ID),
xcm::v3::Junction::GeneralIndex(TELEPORTABLE_ASSET_ID.into()),
]
);
pub PenpalBSiblingSovereignAccount: AccountId = Sibling::from(PENPAL_B_ID).into_account_truncating();
}

/// Helper function to generate a crypto pair from seed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,9 @@ mod imports {
AssetHubWestendParaReceiver as AssetHubWestendReceiver,
AssetHubWestendParaSender as AssetHubWestendSender, BridgeHubRococoPara as BridgeHubRococo,
BridgeHubWestendPara as BridgeHubWestend,
BridgeHubWestendParaSender as BridgeHubWestendSender, PenpalAPara as PenpalA,
PenpalBPara as PenpalB, PenpalBParaSender as PenpalBSender, WestendRelay as Westend,
BridgeHubWestendParaSender as BridgeHubWestendSender, PenpalBPara as PenpalB,
PenpalBParaReceiver as PenpalBReceiver, PenpalBParaSender as PenpalBSender,
WestendRelay as Westend,
};

pub const ASSET_MIN_BALANCE: u128 = 1000;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@ use crate::imports::*;
use asset_hub_westend_runtime::xcm_config::bridging::to_ethereum::DefaultBridgeHubEthereumBaseFee;
use bridge_hub_westend_runtime::EthereumInboundQueue;
use codec::{Decode, Encode};
use emulated_integration_tests_common::PenpalSiblingSovereignAccount;
use emulated_integration_tests_common::{PenpalBSiblingSovereignAccount, TELEPORTABLE_ASSET_ID};
use frame_support::pallet_prelude::TypeInfo;
use hex_literal::hex;
use rococo_westend_system_emulated_network::penpal_emulated_chain::penpal_runtime::xcm_config::RelayLocation;
use snowbridge_core::{outbound::OperatingMode, AssetRegistrarMetadata, TokenIdOf};
use snowbridge_router_primitives::inbound::{
Command, ConvertMessage, Destination, GlobalConsensusEthereumConvertsFor, MessageV1,
Expand Down Expand Up @@ -502,12 +503,9 @@ fn send_relay_token_from_ethereum() {
chain_id: CHAIN_ID,
command: Command::SendNativeToken {
token_id,
destination: Destination::ForeignAccountId32 {
para_id: AssetHubWestend::para_id().into(),
id: AssetHubWestendReceiver::get().into(),
fee: XCM_FEE,
},
destination: Destination::AccountId32 { id: AssetHubWestendReceiver::get().into() },
amount: TOKEN_AMOUNT,
fee: XCM_FEE,
},
});
// Convert the message to XCM
Expand Down Expand Up @@ -569,16 +567,14 @@ fn send_penpal_token_from_ah_to_ethereum() {
BridgeHubWestend::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]);

let penpal_asset_location_on_ah =
Location::new(1, [Junction::Parachain(PenpalA::para_id().into())])
Location::new(1, [Junction::Parachain(PenpalB::para_id().into())])
.appended_with(PenpalLocalTeleportableToAssetHub::get())
.unwrap();
let v3_location: MultiLocation = penpal_asset_location_on_ah.clone().try_into().unwrap();
const TOKEN_AMOUNT: u128 = 100_000_000_000;

AssetHubWestend::mint_foreign_asset(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(
PenpalSiblingSovereignAccount::get().clone(),
),
<AssetHubWestend as Chain>::RuntimeOrigin::signed(PenpalBSiblingSovereignAccount::get()),
v3_location,
AssetHubWestendSender::get(),
TOKEN_AMOUNT,
Expand Down Expand Up @@ -665,7 +661,7 @@ fn send_penpal_token_from_ah_to_ethereum() {
#[test]
fn send_penpal_token_from_ethereum_to_ah() {
let penpal_asset_location_on_ah =
Location::new(1, [Parachain(PenpalA::para_id().into()).into()])
Location::new(1, [Parachain(PenpalB::para_id().into()).into()])
.appended_with(PenpalLocalTeleportableToAssetHub::get())
.unwrap();

Expand All @@ -686,9 +682,7 @@ fn send_penpal_token_from_ethereum_to_ah() {
AssetHubWestend::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]);

AssetHubWestend::mint_foreign_asset(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(
PenpalSiblingSovereignAccount::get().clone(),
),
<AssetHubWestend as Chain>::RuntimeOrigin::signed(PenpalBSiblingSovereignAccount::get()),
v3_location,
ethereum_sovereign.clone(),
TOKEN_AMOUNT,
Expand All @@ -711,12 +705,9 @@ fn send_penpal_token_from_ethereum_to_ah() {
chain_id: CHAIN_ID,
command: Command::SendNativeToken {
token_id,
destination: Destination::ForeignAccountId32 {
para_id: AssetHubWestend::para_id().into(),
id: AssetHubWestendReceiver::get().into(),
fee: XCM_FEE,
},
destination: Destination::AccountId32 { id: AssetHubWestendReceiver::get().into() },
amount: TOKEN_AMOUNT,
fee: XCM_FEE,
},
});
// Convert the message to XCM
Expand Down Expand Up @@ -756,3 +747,125 @@ fn send_penpal_token_from_ethereum_to_ah() {
);
});
}

#[test]
fn send_penpal_token_from_ethereum_to_penpal() {
let penpal_asset_location_on_ah =
Location::new(1, [Parachain(PenpalB::para_id().into()).into()])
.appended_with(PenpalLocalTeleportableToAssetHub::get())
.unwrap();

let v3_location: MultiLocation = penpal_asset_location_on_ah.clone().try_into().unwrap();

let token_id = TokenIdOf::convert_location(&penpal_asset_location_on_ah).unwrap();

const TOKEN_AMOUNT: u128 = 100_000_000_000;

let ethereum_sovereign: AccountId =
GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&Location::new(
2,
[GlobalConsensus(EthereumNetwork::get())],
))
.unwrap()
.into();

AssetHubWestend::fund_accounts(vec![(ethereum_sovereign.clone(), INITIAL_FUND)]);

AssetHubWestend::mint_foreign_asset(
<AssetHubWestend as Chain>::RuntimeOrigin::signed(PenpalBSiblingSovereignAccount::get()),
v3_location.clone(),
ethereum_sovereign.clone(),
TOKEN_AMOUNT,
);

let ah_sovereign =
PenpalB::sovereign_account_id_of(PenpalB::sibling_location_of(AssetHubWestend::para_id()));
PenpalB::fund_accounts(vec![(ah_sovereign.clone(), INITIAL_FUND)]);
PenpalB::mint_foreign_asset(
<PenpalB as Chain>::RuntimeOrigin::signed(PenpalAssetOwner::get()),
RelayLocation::get(),
ah_sovereign.clone(),
INITIAL_FUND,
);
PenpalB::mint_asset(
<PenpalB as Chain>::RuntimeOrigin::signed(PenpalAssetOwner::get()),
TELEPORTABLE_ASSET_ID,
ah_sovereign.clone(),
TOKEN_AMOUNT,
);

BridgeHubWestend::execute_with(|| {
type Runtime = <BridgeHubWestend as Chain>::Runtime;

type RuntimeEvent = <BridgeHubWestend as Chain>::RuntimeEvent;

// create token
snowbridge_pallet_system::Tokens::<Runtime>::insert(
token_id,
VersionedLocation::from(penpal_asset_location_on_ah.clone()),
);

// Send token back to AH
let message_id: H256 = [0; 32].into();
let message = VersionedMessage::V1(MessageV1 {
chain_id: CHAIN_ID,
command: Command::SendNativeToken {
token_id,
destination: Destination::ForeignAccountId32 {
para_id: PenpalB::para_id().into(),
id: PenpalBReceiver::get().into(),
fee: XCM_FEE,
},
amount: TOKEN_AMOUNT,
fee: XCM_FEE,
},
});
// Convert the message to XCM
let (xcm, _) = EthereumInboundQueue::do_convert(message_id, message).unwrap();
// Send the XCM
let _ = EthereumInboundQueue::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap();

assert_expected_events!(
BridgeHubWestend,
vec![
RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) => {},
]
);
});

AssetHubWestend::execute_with(|| {
type RuntimeEvent = <AssetHubWestend as Chain>::RuntimeEvent;

// Check that token burnt from some reserved account
assert_expected_events!(
AssetHubWestend,
vec![
RuntimeEvent::ForeignAssets(pallet_assets::Event::Burned { .. }) => {},
]
);
});

PenpalB::execute_with(|| {
type RuntimeEvent = <PenpalB as Chain>::RuntimeEvent;

// Check that token issued to beneficial
assert_expected_events!(
PenpalB,
vec![
RuntimeEvent::Assets(pallet_assets::Event::Issued { .. }) => {},
]
);

let events = PenpalB::events();

// Check that token issued to destination account
assert!(
events.iter().any(|event| matches!(
event,
RuntimeEvent::Assets(pallet_assets::Event::Issued { amount, owner, ..})
if *amount == TOKEN_AMOUNT && *owner == PenpalBReceiver::get()
)),
"Token minted to beneficiary."
);
})
}

0 comments on commit 0d3988c

Please sign in to comment.