Skip to content

Commit

Permalink
Route first to V2 then fallback to V1 with a custom instruction Expec…
Browse files Browse the repository at this point in the history
…tAssets
  • Loading branch information
yrong committed Oct 16, 2024
1 parent 7a37072 commit db634a3
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 43 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions bridges/snowbridge/primitives/router-v2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ sp-runtime = { workspace = true }
sp-std = { workspace = true }

xcm = { workspace = true }
xcm-builder = { workspace = true }
xcm-executor = { workspace = true }

snowbridge-core = { workspace = true }
Expand All @@ -43,12 +44,14 @@ std = [
"sp-io/std",
"sp-runtime/std",
"sp-std/std",
"xcm-builder/std",
"xcm-executor/std",
"xcm/std",
]
runtime-benchmarks = [
"frame-support/runtime-benchmarks",
"snowbridge-core/runtime-benchmarks",
"sp-runtime/runtime-benchmarks",
"xcm-builder/runtime-benchmarks",
"xcm-executor/runtime-benchmarks",
]
80 changes: 50 additions & 30 deletions bridges/snowbridge/primitives/router-v2/src/outbound/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@
#[cfg(test)]
mod tests;

use core::slice::Iter;

use codec::{Decode, Encode};
use core::slice::Iter;
use sp_std::ops::ControlFlow;

use frame_support::{ensure, traits::Get, BoundedVec};
use frame_support::{
ensure,
traits::{Get, ProcessMessageError},
BoundedVec,
};
use snowbridge_core::{
outbound_v2::{Command, Message, SendMessage},
AgentId, TokenId, TokenIdOf,
Expand All @@ -18,8 +22,11 @@ use sp_core::{H160, H256};
use sp_runtime::traits::MaybeEquivalence;
use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
use xcm::prelude::*;
use xcm_builder::{CreateMatcher, MatchXcm};
use xcm_executor::traits::{ConvertLocation, ExportXcm};

const TARGET: &'static str = "xcm::ethereum_blob_exporter::v2";

pub struct EthereumBlobExporter<
UniversalLocation,
EthereumNetwork,
Expand Down Expand Up @@ -65,39 +72,39 @@ where
let universal_location = UniversalLocation::get();

if network != expected_network {
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}.");
log::trace!(target: TARGET, "skipped due to unmatched bridge network {network:?}.");
return Err(SendError::NotApplicable)
}

// Cloning destination to avoid modifying the value so subsequent exporters can use it.
let dest = destination.clone().take().ok_or(SendError::MissingArgument)?;
if dest != Here {
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}.");
log::trace!(target: TARGET, "skipped due to unmatched remote destination {dest:?}.");
return Err(SendError::NotApplicable)
}

// Cloning universal_source to avoid modifying the value so subsequent exporters can use it.
let (local_net, local_sub) = universal_source.clone()
.take()
.ok_or_else(|| {
log::error!(target: "xcm::ethereum_blob_exporter", "universal source not provided.");
log::error!(target: TARGET, "universal source not provided.");
SendError::MissingArgument
})?
.split_global()
.map_err(|()| {
log::error!(target: "xcm::ethereum_blob_exporter", "could not get global consensus from universal source '{universal_source:?}'.");
log::error!(target: TARGET, "could not get global consensus from universal source '{universal_source:?}'.");
SendError::NotApplicable
})?;

if Ok(local_net) != universal_location.global_consensus() {
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched relay network {local_net:?}.");
log::trace!(target: TARGET, "skipped due to unmatched relay network {local_net:?}.");
return Err(SendError::NotApplicable)
}

let _para_id = match local_sub.as_slice() {
[Parachain(para_id)] => *para_id,
_ => {
log::error!(target: "xcm::ethereum_blob_exporter", "could not get parachain id from universal source '{local_sub:?}'.");
log::error!(target: TARGET, "could not get parachain id from universal source '{local_sub:?}'.");
return Err(SendError::NotApplicable)
},
};
Expand All @@ -107,26 +114,39 @@ where
let agent_id = match AgentHashedDescription::convert_location(&source_location) {
Some(id) => id,
None => {
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to not being able to create agent id. '{source_location:?}'");
log::error!(target: TARGET, "unroutable due to not being able to create agent id. '{source_location:?}'");
return Err(SendError::NotApplicable)
},
};

let message = message.take().ok_or_else(|| {
log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
let message = message.clone().ok_or_else(|| {
log::error!(target: TARGET, "xcm message not provided.");
SendError::MissingArgument
})?;

// An workaround to inspect ExpectAsset as V2 message
let mut instructions = message.clone().0;
let result = instructions.matcher().match_next_inst_while(
|_| true,
|inst| {
return match inst {
ExpectAsset(..) => Err(ProcessMessageError::Unsupported),
_ => Ok(ControlFlow::Continue(())),
}
},
);
ensure!(result.is_err(), SendError::NotApplicable);

let mut converter =
XcmConverter::<ConvertAssetId, ()>::new(&message, expected_network, agent_id);
let message = converter.convert().map_err(|err|{
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
let message = converter.convert().map_err(|err| {
log::error!(target: TARGET, "unroutable due to pattern matching error '{err:?}'.");
SendError::Unroutable
})?;

// validate the message
let (ticket, fee) = OutboundQueue::validate(&message).map_err(|err| {
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}");
log::error!(target: TARGET, "OutboundQueue validation of message failed. {err:?}");
SendError::Unroutable
})?;

Expand All @@ -139,16 +159,16 @@ where
fn deliver(blob: (Vec<u8>, XcmHash)) -> Result<XcmHash, SendError> {
let ticket: OutboundQueue::Ticket = OutboundQueue::Ticket::decode(&mut blob.0.as_ref())
.map_err(|_| {
log::trace!(target: "xcm::ethereum_blob_exporter", "undeliverable due to decoding error");
log::trace!(target: TARGET, "undeliverable due to decoding error");
SendError::NotApplicable
})?;

let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed");
log::error!(target: TARGET, "OutboundQueue submit of message failed");
SendError::Transport("other transport error")
})?;

log::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}.");
log::info!(target: TARGET, "message delivered {message_id:#?}.");
Ok(message_id.into())
}
}
Expand Down Expand Up @@ -232,9 +252,17 @@ where
let _ = self.next();
}

// Get the fee asset item from BuyExecution or continue parsing.
let fee_asset = match_expression!(self.peek(), Ok(BuyExecution { fees, .. }), fees);
if fee_asset.is_some() {
// Extract the fee asset item from BuyExecution
let fee_asset = match_expression!(self.next()?, BuyExecution { fees, .. }, fees)
.ok_or(InvalidFeeAsset)?;
let fee_amount = match fee_asset {
Asset { id: _, fun: Fungible(amount) } => Some(*amount),
_ => None,
}
.ok_or(AssetResolutionFailed)?;

// Check if ExpectAsset exists and skip over it.
if match_expression!(self.peek(), Ok(ExpectAsset { .. }), ()).is_some() {
let _ = self.next();
}

Expand Down Expand Up @@ -268,14 +296,6 @@ where
ensure!(reserve_assets.len() == 1, TooManyAssets);
let reserve_asset = reserve_assets.get(0).ok_or(AssetResolutionFailed)?;

// If there was a fee specified verify it.
if let Some(fee_asset) = fee_asset {
// The fee asset must be the same as the reserve asset.
if fee_asset.id != reserve_asset.id || fee_asset.fun > reserve_asset.fun {
return Err(InvalidFeeAsset)
}
}

let (token, amount) = match reserve_asset {
Asset { id: AssetId(inner_location), fun: Fungible(amount) } =>
match inner_location.unpack() {
Expand All @@ -298,7 +318,7 @@ where
// Todo: from XCMV5 AliasOrigin
origin: H256::zero(),
// Todo: from XCMV5 PayFees
fee: 0,
fee: fee_amount,
commands: BoundedVec::try_from(vec![Command::UnlockNativeToken {
agent_id: self.agent_id,
token,
Expand Down
2 changes: 1 addition & 1 deletion bridges/snowbridge/primitives/router/src/outbound/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ where
},
};

let message = message.take().ok_or_else(|| {
let message = message.clone().ok_or_else(|| {
log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
SendError::MissingArgument
})?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,4 @@ snowbridge-pallet-system = { workspace = true }
snowbridge-pallet-outbound-queue = { workspace = true }
snowbridge-pallet-inbound-queue = { workspace = true }
snowbridge-pallet-inbound-queue-fixtures = { workspace = true }
snowbridge-pallet-outbound-queue-v2 = { workspace = true }
Original file line number Diff line number Diff line change
Expand Up @@ -262,21 +262,12 @@ fn send_weth_asset_from_asset_hub_to_ethereum() {
vec![RuntimeEvent::EthereumOutboundQueue(snowbridge_pallet_outbound_queue::Event::MessageQueued{ .. }) => {},]
);
let events = BridgeHubWestend::events();
// Check that the local fee was credited to the Snowbridge sovereign account
assert!(
events.iter().any(|event| matches!(
event,
RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount })
if *who == TreasuryAccount::get().into() && *amount == 5071000000
)),
"Snowbridge sovereign takes local fee."
);
// Check that the remote fee was credited to the AssetHub sovereign account
assert!(
events.iter().any(|event| matches!(
event,
RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount })
if *who == assethub_sovereign && *amount == 2680000000000,
RuntimeEvent::Balances(pallet_balances::Event::Minted { who,.. })
if *who == assethub_sovereign
)),
"AssetHub sovereign takes remote fee."
);
Expand Down Expand Up @@ -595,3 +586,134 @@ fn transfer_ah_token() {
);
});
}

#[test]
fn send_weth_from_asset_hub_to_ethereum_by_executing_raw_xcm() {
let assethub_location = BridgeHubWestend::sibling_location_of(AssetHubWestend::para_id());
let assethub_sovereign = BridgeHubWestend::sovereign_account_id_of(assethub_location);
let weth_asset_location: Location =
(Parent, Parent, EthereumNetwork::get(), AccountKey20 { network: None, key: WETH }).into();

BridgeHubWestend::fund_accounts(vec![(assethub_sovereign.clone(), INITIAL_FUND)]);

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

assert_ok!(<AssetHubWestend as AssetHubWestendPallet>::ForeignAssets::force_create(
RuntimeOrigin::root(),
weth_asset_location.clone().try_into().unwrap(),
assethub_sovereign.clone().into(),
false,
1,
));

assert!(<AssetHubWestend as AssetHubWestendPallet>::ForeignAssets::asset_exists(
weth_asset_location.clone().try_into().unwrap(),
));
});

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

let message = VersionedMessage::V1(MessageV1 {
chain_id: CHAIN_ID,
command: Command::SendToken {
token: WETH.into(),
destination: Destination::AccountId32 { id: AssetHubWestendReceiver::get().into() },
amount: TOKEN_AMOUNT,
fee: XCM_FEE,
},
});
let (xcm, _) = EthereumInboundQueue::do_convert([0; 32].into(), message).unwrap();
let _ = EthereumInboundQueue::send_xcm(xcm, AssetHubWestend::para_id().into()).unwrap();

// Check that the send token message was sent using xcm
assert_expected_events!(
BridgeHubWestend,
vec![RuntimeEvent::XcmpQueue(cumulus_pallet_xcmp_queue::Event::XcmpMessageSent { .. }) =>{},]
);
});

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

// Check that AssetHub has issued the foreign asset
assert_expected_events!(
AssetHubWestend,
vec![RuntimeEvent::ForeignAssets(pallet_assets::Event::Issued { .. }) => {},]
);

let local_fee_amount = 80_000_000_000;
let remote_fee_amount = 4_000_000_000;
let local_fee_asset =
Asset { id: AssetId(Location::parent()), fun: Fungible(local_fee_amount) };
let remote_fee_asset =
Asset { id: AssetId(weth_asset_location.clone()), fun: Fungible(remote_fee_amount) };
let reserve_asset = Asset {
id: AssetId(weth_asset_location.clone()),
fun: Fungible(TOKEN_AMOUNT - remote_fee_amount),
};
let assets = vec![
Asset { id: AssetId(weth_asset_location.clone()), fun: Fungible(TOKEN_AMOUNT) },
local_fee_asset.clone(),
];
let destination = Location::new(2, [GlobalConsensus(Ethereum { chain_id: CHAIN_ID })]);

let beneficiary = Location::new(
0,
[AccountKey20 { network: None, key: ETHEREUM_DESTINATION_ADDRESS.into() }],
);

// Internal xcm of InitiateReserveWithdraw, WithdrawAssets + ClearOrigin instructions will
// be appended to the front of the list by the xcm executor
let xcm_on_bh = Xcm(vec![
BuyExecution { fees: remote_fee_asset.clone(), weight_limit: Unlimited },
// ExpectAsset as a workaround before XCMv5 to differ Route V1 and V2
ExpectAsset(vec![remote_fee_asset.clone()].into()),
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
]);

let xcms = VersionedXcm::from(Xcm(vec![
WithdrawAsset(assets.clone().into()),
// BuyExecution { fees: local_fee_asset.clone(), weight_limit: Unlimited },
SetFeesMode { jit_withdraw: true },
InitiateReserveWithdraw {
assets: Definite(reserve_asset.clone().into()),
// with reserve set to Ethereum destination, the ExportMessage will
// be appended to the front of the list by the SovereignPaidRemoteExporter
reserve: destination,
xcm: xcm_on_bh,
},
]));

// Send the Weth back to Ethereum
<AssetHubWestend as AssetHubWestendPallet>::PolkadotXcm::execute(
RuntimeOrigin::signed(AssetHubWestendReceiver::get()),
bx!(xcms),
Weight::from(8_000_000_000),
)
.unwrap();
});

BridgeHubWestend::execute_with(|| {
use bridge_hub_westend_runtime::xcm_config::TreasuryAccount;
type RuntimeEvent = <BridgeHubWestend as Chain>::RuntimeEvent;
// Check that the transfer token back to Ethereum message was queue in the Ethereum
// Outbound Queue
assert_expected_events!(
BridgeHubWestend,
vec![RuntimeEvent::EthereumOutboundQueueV2(snowbridge_pallet_outbound_queue_v2::Event::MessageQueued{ .. }) => {},]
);
let events = BridgeHubWestend::events();
// Check that the remote fee was credited to the AssetHub sovereign account
assert!(
events.iter().any(|event| matches!(
event,
RuntimeEvent::Balances(pallet_balances::Event::Minted { who, amount })
if *who == assethub_sovereign && *amount == 2737194500000,
)),
"AssetHub sovereign takes remote fee."
);
});
}
Loading

0 comments on commit db634a3

Please sign in to comment.