Files
pezkuwi-sdk/pezbridges/snowbridge/primitives/inbound-queue/src/v1.rs
T

674 lines
22 KiB
Rust

// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
//! Converts messages from Ethereum to XCM messages
use crate::{CallIndex, EthereumLocationsConverterFor};
use codec::{Decode, DecodeWithMemTracking, Encode};
use core::marker::PhantomData;
use pezframe_support::{traits::tokens::Balance as BalanceT, PalletError};
use pezsnowbridge_core::TokenId;
use pezsp_core::{Get, RuntimeDebug, H160, H256};
use pezsp_runtime::{traits::MaybeConvert, MultiAddress};
use pezsp_std::prelude::*;
use scale_info::TypeInfo;
use xcm::prelude::{Junction::AccountKey20, *};
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` pezpallet
RegisterToken {
/// The address of the ERC20 token to be bridged over to AssetHub
token: H160,
/// XCM execution fee on AssetHub
fee: u128,
},
/// Send Ethereum token to AssetHub or another teyrchain
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,
},
/// Send Pezkuwi token back to the original teyrchain
SendNativeToken {
/// The Id of the token
token_id: TokenId,
/// 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 teyrchain `para_id` on
/// AssetHub, Account `id` on the destination teyrchain will receive the funds via a
/// reserve-backed transfer. See <https://github.com/pezkuwichain/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 teyrchain `para_id` on
/// AssetHub, Account `id` on the destination teyrchain will receive the funds via a
/// reserve-backed transfer. See <https://github.com/pezkuwichain/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,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
> where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
Balance: BalanceT,
ConvertAssetId: MaybeConvert<TokenId, Location>,
EthereumUniversalLocation: Get<InteriorLocation>,
GlobalAssetHubLocation: Get<Location>,
{
_phantom: PhantomData<(
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
)>,
}
/// Reason why a message conversion failed.
#[derive(
Copy, Clone, TypeInfo, PalletError, Encode, Decode, DecodeWithMemTracking, RuntimeDebug,
)]
pub enum ConvertMessageError {
/// The message version is not supported for conversion.
UnsupportedVersion,
InvalidDestination,
InvalidToken,
/// The fee asset is not supported for conversion.
UnsupportedFeeAsset,
CannotReanchor,
}
/// 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_id: H256,
message: VersionedMessage,
) -> Result<(Xcm<()>, Self::Balance), ConvertMessageError>;
}
impl<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
> ConvertMessage
for MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
>
where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
InboundQueuePalletInstance: Get<u8>,
Balance: BalanceT + From<u128>,
AccountId: Into<[u8; 32]>,
ConvertAssetId: MaybeConvert<TokenId, Location>,
EthereumUniversalLocation: Get<InteriorLocation>,
GlobalAssetHubLocation: Get<Location>,
{
type Balance = Balance;
type AccountId = AccountId;
fn convert(
message_id: H256,
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(message_id, chain_id, token, fee)),
V1(MessageV1 { chain_id, command: SendToken { token, destination, amount, fee } }) =>
Ok(Self::convert_send_token(message_id, chain_id, token, destination, amount, fee)),
V1(MessageV1 {
chain_id,
command: SendNativeToken { token_id, destination, amount, fee },
}) => Self::convert_send_native_token(
message_id,
chain_id,
token_id,
destination,
amount,
fee,
),
}
}
}
impl<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
>
MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
ConvertAssetId,
EthereumUniversalLocation,
GlobalAssetHubLocation,
>
where
CreateAssetCall: Get<CallIndex>,
CreateAssetDeposit: Get<u128>,
InboundQueuePalletInstance: Get<u8>,
Balance: BalanceT + From<u128>,
AccountId: Into<[u8; 32]>,
ConvertAssetId: MaybeConvert<TokenId, Location>,
EthereumUniversalLocation: Get<InteriorLocation>,
GlobalAssetHubLocation: Get<Location>,
{
fn convert_register_token(
message_id: H256,
chain_id: u64,
token: H160,
fee: u128,
) -> (Xcm<()>, Balance) {
let network = Ethereum { chain_id };
let xcm_fee: Asset = (Location::parent(), fee).into();
let deposit: Asset = (Location::parent(), CreateAssetDeposit::get()).into();
let total_amount = fee + CreateAssetDeposit::get();
let total: Asset = (Location::parent(), total_amount).into();
let bridge_location = Location::new(2, GlobalConsensus(network));
let owner = EthereumLocationsConverterFor::<[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.clone() },
// This `SetAppendix` ensures that `xcm_fee` not spent by `Transact` will be
// deposited to snowbridge sovereign, instead of being trapped, regardless of
// `Transact` success or not.
SetAppendix(Xcm(vec![
RefundSurplus,
DepositAsset { assets: AllCounted(1).into(), beneficiary: bridge_location },
])),
// Only our inbound-queue pezpallet is allowed to invoke `UniversalOrigin`.
DescendOrigin(PalletInstance(inbound_queue_pallet_index).into()),
// Change origin to the bridge.
UniversalOrigin(GlobalConsensus(network)),
// Call create_asset on foreign assets pezpallet.
Transact {
origin_kind: OriginKind::Xcm,
fallback_max_weight: Some(Weight::from_parts(400_000_000, 8_000)),
call: (
create_call_index,
asset_id,
MultiAddress::<[u8; 32], ()>::Id(owner),
MINIMUM_DEPOSIT,
)
.encode()
.into(),
},
// Forward message id to Asset Hub
SetTopic(message_id.into()),
// Once the program ends here, appendix program will run, which will deposit any
// leftover fee to snowbridge sovereign.
]
.into();
(xcm, total_amount.into())
}
fn convert_send_token(
message_id: H256,
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: Asset = (Location::parent(), asset_hub_fee).into();
let asset: Asset = (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, Location::new(0, [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),
Location::new(0, [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),
Location::new(0, [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: Asset = (Location::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(PalletInstance(inbound_queue_pallet_index).into()),
UniversalOrigin(GlobalConsensus(network)),
ReserveAssetDeposited(asset.clone().into()),
ClearOrigin,
];
match dest_para_id {
Some(dest_para_id) => {
let dest_para_fee_asset: Asset = (Location::parent(), dest_para_fee).into();
let bridge_location = Location::new(2, GlobalConsensus(network));
instructions.extend(vec![
// After program finishes deposit any leftover assets to the snowbridge
// sovereign.
SetAppendix(Xcm(vec![DepositAsset {
assets: Wild(AllCounted(2)),
beneficiary: bridge_location,
}])),
// Perform a deposit reserve to send to destination chain.
DepositReserveAsset {
// Send over assets and unspent fees, XCM delivery fee will be charged from
// here.
assets: Wild(AllCounted(2)),
dest: Location::new(1, [Teyrchain(dest_para_id)]),
xcm: vec![
// Buy execution on target.
BuyExecution { fees: dest_para_fee_asset, weight_limit: Unlimited },
// Deposit assets to beneficiary.
DepositAsset { assets: Wild(AllCounted(2)), beneficiary },
// Forward message id to destination teyrchain.
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()));
// The `instructions` to forward to AssetHub, and the `total_fees` to locally burn (since
// they are teleported within `instructions`).
(instructions.into(), total_fees.into())
}
// Convert ERC20 token address to a location that can be understood by Assets Hub.
fn convert_token_address(network: NetworkId, token: H160) -> Location {
if token == H160([0; 20]) {
Location::new(2, [GlobalConsensus(network)])
} else {
Location::new(
2,
[GlobalConsensus(network), AccountKey20 { network: None, key: token.into() }],
)
}
}
/// Constructs an XCM message destined for AssetHub that withdraws assets from the sovereign
/// account of the Gateway contract and either deposits those assets into a recipient account or
/// forwards the assets to another teyrchain.
fn convert_send_native_token(
message_id: H256,
chain_id: u64,
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 = match destination {
// Final destination is a 32-byte account on AssetHub
Destination::AccountId32 { id } =>
Ok(Location::new(0, [AccountId32 { network: None, id }])),
// Forwarding to a destination teyrchain is not allowed for PNA and is validated on the
// Ethereum side. https://github.com/Snowfork/snowbridge/blob/e87ddb2215b513455c844463a25323bb9c01ff36/contracts/src/Assets.sol#L216-L224
_ => Err(ConvertMessageError::InvalidDestination),
}?;
let total_fee_asset: Asset = (Location::parent(), asset_hub_fee).into();
let asset_loc =
ConvertAssetId::maybe_convert(token_id).ok_or(ConvertMessageError::InvalidToken)?;
let mut reanchored_asset_loc = asset_loc.clone();
reanchored_asset_loc
.reanchor(&GlobalAssetHubLocation::get(), &EthereumUniversalLocation::get())
.map_err(|_| ConvertMessageError::CannotReanchor)?;
let asset: Asset = (reanchored_asset_loc, amount).into();
let inbound_queue_pallet_index = InboundQueuePalletInstance::get();
let 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()),
// 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 },
SetTopic(message_id.into()),
];
// `total_fees` to burn on this chain when sending `instructions` to run on AH (which also
// teleport fees)
Ok((instructions.into(), asset_hub_fee.into()))
}
}
#[cfg(test)]
mod tests {
use crate::{
v1::{Command, ConvertMessage, Destination, MessageToXcm, MessageV1, VersionedMessage},
CallIndex, EthereumLocationsConverterFor,
};
use hex_literal::hex;
use pezframe_support::{assert_ok, parameter_types};
use pezsnowbridge_test_utils::mock_converter::{
add_location_override, reanchor_to_ethereum, LocationIdConvert,
};
use pezsp_core::H160;
use pezsp_runtime::{
traits::{IdentifyAccount, Verify},
MultiSignature,
};
use xcm::prelude::*;
use xcm_executor::traits::ConvertLocation;
pub const CHAIN_ID: u64 = 1;
const NETWORK: NetworkId = Ethereum { chain_id: CHAIN_ID };
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;
pub const InboundQueuePalletInstance: u8 = 80;
pub EthereumUniversalLocation: InteriorLocation =
[GlobalConsensus(NETWORK)].into();
pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(Pezkuwi),Teyrchain(1000)]);
pub EthereumLocation: Location = Location::new(2,EthereumUniversalLocation::get());
pub BridgeHubContext: InteriorLocation = [GlobalConsensus(Pezkuwi),Teyrchain(1002)].into();
}
type AccountId = <<MultiSignature as Verify>::Signer as IdentifyAccount>::AccountId;
type Balance = u128;
pub type MessageConverter = MessageToXcm<
CreateAssetCall,
CreateAssetDeposit,
InboundQueuePalletInstance,
AccountId,
Balance,
LocationIdConvert,
EthereumUniversalLocation,
AssetHubFromEthereum,
>;
#[test]
fn test_contract_location_with_network_converts_successfully() {
let expected_account: [u8; 32] =
hex!("204dfe37731e8e2b4866ad0da9a17c49f434542c3477c5f914a3349acd88ba1a");
let contract_location = Location::new(2, [GlobalConsensus(NETWORK)]);
let account =
EthereumLocationsConverterFor::<[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 = Location::new(2, [GlobalConsensus(Pezkuwi), Teyrchain(1000)]);
assert_eq!(
EthereumLocationsConverterFor::<[u8; 32]>::convert_location(&contract_location),
None,
);
}
#[test]
fn test_reanchor_all_assets() {
let ethereum_context: InteriorLocation = [GlobalConsensus(Ethereum { chain_id: 1 })].into();
let ethereum = Location::new(2, ethereum_context.clone());
let ah_context: InteriorLocation = [GlobalConsensus(Pezkuwi), Teyrchain(1000)].into();
let global_ah = Location::new(1, ah_context.clone());
let assets = vec![
// HEZ
Location::new(1, []),
// GLMR (Some Pezkuwi teyrchain currency)
Location::new(1, [Teyrchain(2004)]),
// AH asset
Location::new(0, [PalletInstance(50), GeneralIndex(42)]),
// KSM
Location::new(2, [GlobalConsensus(Kusama)]),
// KAR (Some Kusama teyrchain currency)
Location::new(2, [GlobalConsensus(Kusama), Teyrchain(2000)]),
];
for asset in assets.iter() {
// reanchor logic in pezpallet_xcm on AH
let mut reanchored_asset = asset.clone();
assert_ok!(reanchored_asset.reanchor(&ethereum, &ah_context));
// reanchor back to original location in context of Ethereum
let mut reanchored_asset_with_ethereum_context = reanchored_asset.clone();
assert_ok!(
reanchored_asset_with_ethereum_context.reanchor(&global_ah, &ethereum_context)
);
assert_eq!(reanchored_asset_with_ethereum_context, asset.clone());
}
}
#[test]
fn test_convert_send_weth() {
const WETH: H160 = H160([0xff; 20]);
const AMOUNT: u128 = 1_000_000;
const FEE: u128 = 1_000;
const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 {
chain_id: CHAIN_ID,
command: Command::SendToken {
token: WETH,
destination: Destination::AccountId32 { id: ACCOUNT_ID },
amount: AMOUNT,
fee: FEE,
},
});
let result = MessageConverter::convert([1; 32].into(), MESSAGE);
assert_ok!(&result);
let (xcm, fee) = result.unwrap();
assert_eq!(FEE, fee);
let expected_assets = ReserveAssetDeposited(
vec![Asset {
id: AssetId(Location {
parents: 2,
interior: Junctions::X2(
[
GlobalConsensus(NETWORK),
AccountKey20 { network: None, key: WETH.into() },
]
.into(),
),
}),
fun: Fungible(AMOUNT),
}]
.into(),
);
let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..)));
assert_eq!(actual_assets, Some(expected_assets))
}
#[test]
fn test_convert_send_eth() {
const ETH: H160 = H160([0x00; 20]);
const AMOUNT: u128 = 1_000_000;
const FEE: u128 = 1_000;
const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
const MESSAGE: VersionedMessage = VersionedMessage::V1(MessageV1 {
chain_id: CHAIN_ID,
command: Command::SendToken {
token: ETH,
destination: Destination::AccountId32 { id: ACCOUNT_ID },
amount: AMOUNT,
fee: FEE,
},
});
let result = MessageConverter::convert([1; 32].into(), MESSAGE);
assert_ok!(&result);
let (xcm, fee) = result.unwrap();
assert_eq!(FEE, fee);
let expected_assets = ReserveAssetDeposited(
vec![Asset {
id: AssetId(Location {
parents: 2,
interior: Junctions::X1([GlobalConsensus(NETWORK)].into()),
}),
fun: Fungible(AMOUNT),
}]
.into(),
);
let actual_assets = xcm.into_iter().find(|x| matches!(x, ReserveAssetDeposited(..)));
assert_eq!(actual_assets, Some(expected_assets))
}
#[test]
fn test_convert_send_dot() {
let dot_location = Location::parent();
let (token_id, _) = reanchor_to_ethereum(
dot_location.clone(),
EthereumLocation::get(),
BridgeHubContext::get(),
);
add_location_override(
dot_location.clone(),
EthereumLocation::get(),
BridgeHubContext::get(),
);
const AMOUNT: u128 = 1_000_000;
const FEE: u128 = 1_000;
const ACCOUNT_ID: [u8; 32] = [0xBA; 32];
let message: VersionedMessage = VersionedMessage::V1(MessageV1 {
chain_id: CHAIN_ID,
command: Command::SendNativeToken {
token_id,
destination: Destination::AccountId32 { id: ACCOUNT_ID },
amount: AMOUNT,
fee: FEE,
},
});
let result = MessageConverter::convert([1; 32].into(), message);
assert_ok!(&result);
let (xcm, fee) = result.unwrap();
assert_eq!(FEE, fee);
let expected_assets = WithdrawAsset(
vec![Asset { id: AssetId(Location::parent()), fun: Fungible(AMOUNT) }].into(),
);
let actual_assets = xcm.into_iter().find(|x| matches!(x, WithdrawAsset(..)));
assert_eq!(actual_assets, Some(expected_assets))
}
}