Adds Snowbridge to Rococo runtime (#2522)

# Description

Adds Snowbridge to the Rococo bridge hub runtime. Includes config
changes required in Rococo asset hub.

---------

Co-authored-by: Alistair Singh <alistair.singh7@gmail.com>
Co-authored-by: ron <yrong1997@gmail.com>
Co-authored-by: Vincent Geddes <vincent.geddes@hey.com>
Co-authored-by: claravanstaden <Cats 4 life!>
This commit is contained in:
Clara van Staden
2023-12-21 18:06:36 +02:00
committed by GitHub
parent 9f5221cc2f
commit 18d53dbf91
151 changed files with 19379 additions and 149 deletions
@@ -0,0 +1,320 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Converts messages from Ethereum to XCM messages
#[cfg(test)]
mod tests;
use codec::{Decode, Encode};
use core::marker::PhantomData;
use frame_support::{traits::tokens::Balance as BalanceT, weights::Weight, PalletError};
use scale_info::TypeInfo;
use sp_core::{Get, RuntimeDebug, H160};
use sp_io::hashing::blake2_256;
use sp_runtime::MultiAddress;
use sp_std::prelude::*;
use xcm::prelude::{Junction::AccountKey20, *};
use xcm_executor::traits::ConvertLocation;
const MINIMUM_DEPOSIT: u128 = 1;
/// Messages from Ethereum are versioned. This is because in future,
/// we may want to evolve the protocol so that the ethereum side sends XCM messages directly.
/// Instead having BridgeHub transcode the messages into XCM.
#[derive(Clone, Encode, Decode, RuntimeDebug)]
pub enum VersionedMessage {
V1(MessageV1),
}
/// For V1, the ethereum side sends messages which are transcoded into XCM. These messages are
/// self-contained, in that they can be transcoded using only information in the message.
#[derive(Clone, Encode, Decode, RuntimeDebug)]
pub struct MessageV1 {
/// EIP-155 chain id of the origin Ethereum network
pub chain_id: u64,
/// The command originating from the Gateway contract
pub command: Command,
}
#[derive(Clone, Encode, Decode, RuntimeDebug)]
pub enum Command {
/// Register a wrapped token on the AssetHub `ForeignAssets` pallet
RegisterToken {
/// The address of the ERC20 token to be bridged over to AssetHub
token: H160,
/// XCM execution fee on AssetHub
fee: u128,
},
/// Send a token to AssetHub or another parachain
SendToken {
/// The address of the ERC20 token to be bridged over to AssetHub
token: H160,
/// The destination for the transfer
destination: Destination,
/// Amount to transfer
amount: u128,
/// XCM execution fee on AssetHub
fee: u128,
},
}
/// Destination for bridged tokens
#[derive(Clone, Encode, Decode, RuntimeDebug)]
pub enum Destination {
/// The funds will be deposited into account `id` on AssetHub
AccountId32 { id: [u8; 32] },
/// The funds will deposited into the sovereign account of destination parachain `para_id` on
/// AssetHub, Account `id` on the destination parachain will receive the funds via a
/// reserve-backed transfer. See <https://github.com/paritytech/xcm-format#depositreserveasset>
ForeignAccountId32 {
para_id: u32,
id: [u8; 32],
/// XCM execution fee on final destination
fee: u128,
},
/// The funds will deposited into the sovereign account of destination parachain `para_id` on
/// AssetHub, Account `id` on the destination parachain will receive the funds via a
/// reserve-backed transfer. See <https://github.com/paritytech/xcm-format#depositreserveasset>
ForeignAccountId20 {
para_id: u32,
id: [u8; 20],
/// XCM execution fee on final destination
fee: u128,
},
}
pub struct MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
> where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
Balance: BalanceT,
{
_phantom: PhantomData<(
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
)>,
}
/// Reason why a message conversion failed.
#[derive(Copy, Clone, TypeInfo, PalletError, Encode, Decode, RuntimeDebug)]
pub enum ConvertMessageError {
/// The message version is not supported for conversion.
UnsupportedVersion,
}
/// convert the inbound message to xcm which will be forwarded to the destination chain
pub trait ConvertMessage {
type Balance: BalanceT + From<u128>;
type AccountId;
/// Converts a versioned message into an XCM message and an optional topicID
fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
}
pub type CallIndex = [u8; 2];
impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
ConvertMessage
for MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
> where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
InboundQueuePalletInstance: Get<u8>,
Balance: BalanceT + From<u128>,
AccountId: Into<[u8; 32]>,
{
type Balance = Balance;
type AccountId = AccountId;
fn convert(message: VersionedMessage) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError> {
use Command::*;
use VersionedMessage::*;
match message {
V1(MessageV1 { chain_id, command: RegisterToken { token, fee } }) =>
Ok(Self::convert_register_token(chain_id, token, fee)),
V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) =>
Ok(Self::convert_send_token(chain_id, token, destination, amount, fee)),
}
}
}
impl<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
MessageToXcm<CreateAssetCall, CreateAssetDeposit, InboundQueuePalletInstance, AccountId, Balance>
where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
InboundQueuePalletInstance: Get<u8>,
Balance: BalanceT + From<u128>,
AccountId: Into<[u8; 32]>,
{
fn convert_register_token(chain_id: u64, token: H160, fee: u128) -> (Xcm<()>, Balance) {
let network = Ethereum { chain_id };
let xcm_fee: MultiAsset = (MultiLocation::parent(), fee).into();
let deposit: MultiAsset = (MultiLocation::parent(), CreateAssetDeposit::get()).into();
let total_amount = fee + CreateAssetDeposit::get();
let total: MultiAsset = (MultiLocation::parent(), total_amount).into();
let bridge_location: MultiLocation = (Parent, Parent, GlobalConsensus(network)).into();
let owner = GlobalConsensusEthereumConvertsFor::<[u8; 32]>::from_chain_id(&chain_id);
let asset_id = Self::convert_token_address(network, token);
let create_call_index: [u8; 2] = CreateAssetCall::get();
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
let xcm: Xcm<()> = vec![
// Teleport required fees.
ReceiveTeleportedAsset(total.into()),
// Pay for execution.
BuyExecution { fees: xcm_fee, weight_limit: Unlimited },
// Fund the snowbridge sovereign with the required deposit for creation.
DepositAsset { assets: Definite(deposit.into()), beneficiary: bridge_location },
// Only our inbound-queue pallet is allowed to invoke `UniversalOrigin`
DescendOrigin(X1(PalletInstance(inbound_queue_pallet_index))),
// Change origin to the bridge.
UniversalOrigin(GlobalConsensus(network)),
// Call create_asset on foreign assets pallet.
Transact {
origin_kind: OriginKind::Xcm,
require_weight_at_most: Weight::from_parts(400_000_000, 8_000),
call: (
create_call_index,
asset_id,
MultiAddress::<[u8; 32], ()>::Id(owner),
MINIMUM_DEPOSIT,
)
.encode()
.into(),
},
RefundSurplus,
// Clear the origin so that remaining assets in holding
// are claimable by the physical origin (BridgeHub)
ClearOrigin,
]
.into();
(xcm, total_amount.into())
}
fn convert_send_token(
chain_id: u64,
token: H160,
destination: Destination,
amount: u128,
asset_hub_fee: u128,
) -> (Xcm<()>, Balance) {
let network = Ethereum { chain_id };
let asset_hub_fee_asset: MultiAsset = (MultiLocation::parent(), asset_hub_fee).into();
let asset: MultiAsset = (Self::convert_token_address(network, token), amount).into();
let (dest_para_id, beneficiary, dest_para_fee) = match destination {
// Final destination is a 32-byte account on AssetHub
Destination::AccountId32 { id } => (
None,
MultiLocation { parents: 0, interior: X1(AccountId32 { network: None, id }) },
0,
),
// Final destination is a 32-byte account on a sibling of AssetHub
Destination::ForeignAccountId32 { para_id, id, fee } => (
Some(para_id),
MultiLocation { parents: 0, interior: X1(AccountId32 { network: None, id }) },
// Total fee needs to cover execution on AssetHub and Sibling
fee,
),
// Final destination is a 20-byte account on a sibling of AssetHub
Destination::ForeignAccountId20 { para_id, id, fee } => (
Some(para_id),
MultiLocation { parents: 0, interior: X1(AccountKey20 { network: None, key: id }) },
// Total fee needs to cover execution on AssetHub and Sibling
fee,
),
};
let total_fees = asset_hub_fee.saturating_add(dest_para_fee);
let total_fee_asset: MultiAsset = (MultiLocation::parent(), total_fees).into();
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
let mut instructions = vec![
ReceiveTeleportedAsset(total_fee_asset.into()),
BuyExecution { fees: asset_hub_fee_asset, weight_limit: Unlimited },
DescendOrigin(X1(PalletInstance(inbound_queue_pallet_index))),
UniversalOrigin(GlobalConsensus(network)),
ReserveAssetDeposited(asset.clone().into()),
ClearOrigin,
];
match dest_para_id {
Some(dest_para_id) => {
let dest_para_fee_asset: MultiAsset =
(MultiLocation::parent(), dest_para_fee).into();
instructions.extend(vec![
// Perform a deposit reserve to send to destination chain.
DepositReserveAsset {
assets: Definite(vec![dest_para_fee_asset.clone(), asset.clone()].into()),
dest: MultiLocation { parents: 1, interior: X1(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: Definite(asset.into()), beneficiary },
]
.into(),
},
]);
},
None => {
instructions.extend(vec![
// Deposit asset to beneficiary.
DepositAsset { assets: Definite(asset.into()), beneficiary },
]);
},
}
(instructions.into(), total_fees.into())
}
// Convert ERC20 token address to a Multilocation that can be understood by Assets Hub.
fn convert_token_address(network: NetworkId, token: H160) -> MultiLocation {
MultiLocation {
parents: 2,
interior: X2(
GlobalConsensus(network),
AccountKey20 { network: None, key: token.into() },
),
}
}
}
pub struct GlobalConsensusEthereumConvertsFor<AccountId>(PhantomData<AccountId>);
impl<AccountId> ConvertLocation<AccountId> for GlobalConsensusEthereumConvertsFor<AccountId>
where
AccountId: From<[u8; 32]> + Clone,
{
fn convert_location(location: &MultiLocation) -> Option<AccountId> {
if let MultiLocation { interior: X1(GlobalConsensus(Ethereum { chain_id })), .. } = location
{
Some(Self::from_chain_id(chain_id).into())
} else {
None
}
}
}
impl<AccountId> GlobalConsensusEthereumConvertsFor<AccountId> {
pub fn from_chain_id(chain_id: &u64) -> [u8; 32] {
(b"ethereum-chain", chain_id).using_encoded(blake2_256)
}
}
@@ -0,0 +1,41 @@
use super::GlobalConsensusEthereumConvertsFor;
use crate::inbound::CallIndex;
use frame_support::parameter_types;
use hex_literal::hex;
use xcm::v3::prelude::*;
use xcm_executor::traits::ConvertLocation;
const NETWORK: NetworkId = Ethereum { chain_id: 11155111 };
parameter_types! {
pub EthereumNetwork: NetworkId = NETWORK;
pub const CreateAssetCall: CallIndex = [1, 1];
pub const CreateAssetExecutionFee: u128 = 123;
pub const CreateAssetDeposit: u128 = 891;
pub const SendTokenExecutionFee: u128 = 592;
}
#[test]
fn test_contract_location_with_network_converts_successfully() {
let expected_account: [u8; 32] =
hex!("ce796ae65569a670d0c1cc1ac12515a3ce21b5fbf729d63d7b289baad070139d");
let contract_location = MultiLocation { parents: 2, interior: X1(GlobalConsensus(NETWORK)) };
let account =
GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location)
.unwrap();
assert_eq!(account, expected_account);
}
#[test]
fn test_contract_location_with_incorrect_location_fails_convert() {
let contract_location =
MultiLocation { parents: 2, interior: X2(GlobalConsensus(Polkadot), Parachain(1000)) };
assert_eq!(
GlobalConsensusEthereumConvertsFor::<[u8; 32]>::convert_location(&contract_location),
None,
);
}
@@ -0,0 +1,6 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
#![cfg_attr(not(feature = "std"), no_std)]
pub mod inbound;
pub mod outbound;
@@ -0,0 +1,282 @@
// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Converts XCM messages into simpler commands that can be processed by the Gateway contract
#[cfg(test)]
mod tests;
use core::slice::Iter;
use codec::{Decode, Encode};
use frame_support::{ensure, traits::Get};
use snowbridge_core::{
outbound::{AgentExecuteCommand, Command, Message, SendMessage},
ChannelId, ParaId,
};
use sp_core::{H160, H256};
use sp_std::{iter::Peekable, marker::PhantomData, prelude::*};
use xcm::v3::prelude::*;
use xcm_executor::traits::{ConvertLocation, ExportXcm};
pub struct EthereumBlobExporter<
UniversalLocation,
EthereumNetwork,
OutboundQueue,
AgentHashedDescription,
>(PhantomData<(UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription)>);
impl<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription> ExportXcm
for EthereumBlobExporter<UniversalLocation, EthereumNetwork, OutboundQueue, AgentHashedDescription>
where
UniversalLocation: Get<InteriorMultiLocation>,
EthereumNetwork: Get<NetworkId>,
OutboundQueue: SendMessage<Balance = u128>,
AgentHashedDescription: ConvertLocation<H256>,
{
type Ticket = (Vec<u8>, XcmHash);
fn validate(
network: NetworkId,
_channel: u32,
universal_source: &mut Option<InteriorMultiLocation>,
destination: &mut Option<InteriorMultiLocation>,
message: &mut Option<Xcm<()>>,
) -> SendResult<Self::Ticket> {
let expected_network = EthereumNetwork::get();
let universal_location = UniversalLocation::get();
if network != expected_network {
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched bridge network {network:?}.");
return Err(SendError::NotApplicable)
}
let dest = destination.take().ok_or(SendError::MissingArgument)?;
if dest != Here {
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched remote destination {dest:?}.");
return Err(SendError::NotApplicable)
}
let (local_net, local_sub) = universal_source
.take()
.ok_or_else(|| {
log::error!(target: "xcm::ethereum_blob_exporter", "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:?}'.");
SendError::Unroutable
})?;
if Ok(local_net) != universal_location.global_consensus() {
log::trace!(target: "xcm::ethereum_blob_exporter", "skipped due to unmatched relay network {local_net:?}.");
return Err(SendError::NotApplicable)
}
let para_id = match local_sub {
X1(Parachain(para_id)) => para_id,
_ => {
log::error!(target: "xcm::ethereum_blob_exporter", "could not get parachain id from universal source '{local_sub:?}'.");
return Err(SendError::MissingArgument)
},
};
let message = message.take().ok_or_else(|| {
log::error!(target: "xcm::ethereum_blob_exporter", "xcm message not provided.");
SendError::MissingArgument
})?;
let mut converter = XcmConverter::new(&message, &expected_network);
let (agent_execute_command, message_id) = converter.convert().map_err(|err|{
log::error!(target: "xcm::ethereum_blob_exporter", "unroutable due to pattern matching error '{err:?}'.");
SendError::Unroutable
})?;
let source_location: MultiLocation = MultiLocation { parents: 1, interior: local_sub };
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:?}'");
return Err(SendError::Unroutable)
},
};
let channel_id: ChannelId = ParaId::from(para_id).into();
let outbound_message = Message {
id: Some(message_id.into()),
channel_id,
command: Command::AgentExecute { agent_id, command: agent_execute_command },
};
// validate the message
let (ticket, fee) = OutboundQueue::validate(&outbound_message).map_err(|err| {
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue validation of message failed. {err:?}");
SendError::Unroutable
})?;
// convert fee to MultiAsset
let fee = MultiAsset::from((MultiLocation::parent(), fee.total())).into();
Ok(((ticket.encode(), message_id), fee))
}
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");
SendError::NotApplicable
})?;
let message_id = OutboundQueue::deliver(ticket).map_err(|_| {
log::error!(target: "xcm::ethereum_blob_exporter", "OutboundQueue submit of message failed");
SendError::Transport("other transport error")
})?;
log::info!(target: "xcm::ethereum_blob_exporter", "message delivered {message_id:#?}.");
Ok(message_id.into())
}
}
/// Errors that can be thrown to the pattern matching step.
#[derive(PartialEq, Debug)]
enum XcmConverterError {
UnexpectedEndOfXcm,
EndOfXcmMessageExpected,
WithdrawAssetExpected,
DepositAssetExpected,
NoReserveAssets,
FilterDoesNotConsumeAllAssets,
TooManyAssets,
ZeroAssetTransfer,
BeneficiaryResolutionFailed,
AssetResolutionFailed,
InvalidFeeAsset,
SetTopicExpected,
}
macro_rules! match_expression {
($expression:expr, $(|)? $( $pattern:pat_param )|+ $( if $guard: expr )?, $value:expr $(,)?) => {
match $expression {
$( $pattern )|+ $( if $guard )? => Some($value),
_ => None,
}
};
}
struct XcmConverter<'a, Call> {
iter: Peekable<Iter<'a, Instruction<Call>>>,
ethereum_network: &'a NetworkId,
}
impl<'a, Call> XcmConverter<'a, Call> {
fn new(message: &'a Xcm<Call>, ethereum_network: &'a NetworkId) -> Self {
Self { iter: message.inner().iter().peekable(), ethereum_network }
}
fn convert(&mut self) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
// Get withdraw/deposit and make native tokens create message.
let result = self.native_tokens_unlock_message()?;
// All xcm instructions must be consumed before exit.
if self.next().is_ok() {
return Err(XcmConverterError::EndOfXcmMessageExpected)
}
Ok(result)
}
fn native_tokens_unlock_message(
&mut self,
) -> Result<(AgentExecuteCommand, [u8; 32]), XcmConverterError> {
use XcmConverterError::*;
// Get the reserve assets from WithdrawAsset.
let reserve_assets =
match_expression!(self.next()?, WithdrawAsset(reserve_assets), reserve_assets)
.ok_or(WithdrawAssetExpected)?;
// Check if clear origin exists and skip over it.
if match_expression!(self.peek(), Ok(ClearOrigin), ()).is_some() {
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() {
let _ = self.next();
}
let (deposit_assets, beneficiary) = match_expression!(
self.next()?,
DepositAsset { assets, beneficiary },
(assets, beneficiary)
)
.ok_or(DepositAssetExpected)?;
// assert that the beneficiary is AccountKey20.
let recipient = match_expression!(
beneficiary,
MultiLocation { parents: 0, interior: X1(AccountKey20 { network, key }) }
if self.network_matches(network),
H160(*key)
)
.ok_or(BeneficiaryResolutionFailed)?;
// Make sure there are reserved assets.
if reserve_assets.len() == 0 {
return Err(NoReserveAssets)
}
// Check the the deposit asset filter matches what was reserved.
if reserve_assets.inner().iter().any(|asset| !deposit_assets.matches(asset)) {
return Err(FilterDoesNotConsumeAllAssets)
}
// We only support a single asset at a time.
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_expression!(
reserve_asset,
MultiAsset {
id: Concrete(MultiLocation { parents: 0, interior: X1(AccountKey20 { network , key })}),
fun: Fungible(amount)
} if self.network_matches(network),
(H160(*key), *amount)
)
.ok_or(AssetResolutionFailed)?;
// transfer amount must be greater than 0.
ensure!(amount > 0, ZeroAssetTransfer);
// Check if there is a SetTopic and skip over it if found.
let topic_id = match_expression!(self.next()?, SetTopic(id), id).ok_or(SetTopicExpected)?;
Ok((AgentExecuteCommand::TransferToken { token, recipient, amount }, *topic_id))
}
fn next(&mut self) -> Result<&'a Instruction<Call>, XcmConverterError> {
self.iter.next().ok_or(XcmConverterError::UnexpectedEndOfXcm)
}
fn peek(&mut self) -> Result<&&'a Instruction<Call>, XcmConverterError> {
self.iter.peek().ok_or(XcmConverterError::UnexpectedEndOfXcm)
}
fn network_matches(&self, network: &Option<NetworkId>) -> bool {
if let Some(network) = network {
network == self.ethereum_network
} else {
true
}
}
}
File diff suppressed because it is too large Load Diff