fix: EnsureOrigin try_successful_origin and snowbridge rename
- Fix pezpallet-welati EnsureOrigin implementations (3 fixes)
- Remove incorrect #[cfg(not(feature = "runtime-benchmarks"))] blocks
- Affects EnsureSerok, EnsureParlementer, EnsureDiwan
- Fix asset-hub-zagros governance origins macros (2 fixes)
- Remove non-benchmark try_successful_origin from decl_unit_ensures!
- Remove non-benchmark try_successful_origin from decl_ensure!
- Rename snowbridge -> pezsnowbridge for consistency
- Update WORKFLOW_PLAN.md with build status and package names
- Correct package names: pezkuwi-teyrchain-bin, pezstaging-node-cli
- Mark completed builds: pezkuwi, pezkuwi-teyrchain-bin,
pezstaging-node-cli, teyrchain-template-node
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
// SPDX-FileCopyrightText: 2021-2022 Parity Technologies (UK) Ltd.
|
||||
#![cfg_attr(not(feature = "std"), no_std)]
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
use codec::Encode;
|
||||
use pezsp_core::blake2_256;
|
||||
use pezsp_std::marker::PhantomData;
|
||||
use xcm::prelude::{AccountKey20, Ethereum, GlobalConsensus, Location};
|
||||
use xcm_executor::traits::ConvertLocation;
|
||||
|
||||
pub use pezsnowbridge_verification_primitives::*;
|
||||
|
||||
/// DEPRECATED in favor of [xcm_builder::ExternalConsensusLocationsConverterFor]
|
||||
pub struct EthereumLocationsConverterFor<AccountId>(PhantomData<AccountId>);
|
||||
impl<AccountId> ConvertLocation<AccountId> for EthereumLocationsConverterFor<AccountId>
|
||||
where
|
||||
AccountId: From<[u8; 32]> + Clone,
|
||||
{
|
||||
fn convert_location(location: &Location) -> Option<AccountId> {
|
||||
match location.unpack() {
|
||||
(2, [GlobalConsensus(Ethereum { chain_id })]) => {
|
||||
Some(Self::from_chain_id(chain_id).into())
|
||||
},
|
||||
(2, [GlobalConsensus(Ethereum { chain_id }), AccountKey20 { network: _, key }]) => {
|
||||
Some(Self::from_chain_id_with_key(chain_id, *key).into())
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<AccountId> EthereumLocationsConverterFor<AccountId> {
|
||||
pub fn from_chain_id(chain_id: &u64) -> [u8; 32] {
|
||||
(b"ethereum-chain", chain_id).using_encoded(blake2_256)
|
||||
}
|
||||
pub fn from_chain_id_with_key(chain_id: &u64, key: [u8; 20]) -> [u8; 32] {
|
||||
(b"ethereum-chain", chain_id, key).using_encoded(blake2_256)
|
||||
}
|
||||
}
|
||||
|
||||
pub type CallIndex = [u8; 2];
|
||||
@@ -0,0 +1,677 @@
|
||||
// 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/polkadot-fellows/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/polkadot-fellows/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(ðereum, &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, ðereum_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))
|
||||
}
|
||||
}
|
||||
@@ -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 pezframe_support::ensure;
|
||||
use pezsnowbridge_core::{ParaId, TokenId};
|
||||
use pezsp_core::{Get, RuntimeDebug, H160};
|
||||
use pezsp_io::hashing::blake2_256;
|
||||
use pezsp_runtime::{traits::MaybeConvert, MultiAddress};
|
||||
use pezsp_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` pezpallet
|
||||
/// 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 hex_literal::hex;
|
||||
use pezframe_support::{assert_err, assert_ok, parameter_types};
|
||||
use pezsnowbridge_core::TokenId;
|
||||
use pezsnowbridge_test_utils::mock_converter::{
|
||||
add_location_override, reanchor_to_ethereum, LocationIdConvert,
|
||||
};
|
||||
use pezsp_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() {
|
||||
pezsp_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 pezpallet 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() {
|
||||
pezsp_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() {
|
||||
pezsp_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() {
|
||||
pezsp_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() {
|
||||
pezsp_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() {
|
||||
pezsp_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");
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2023 Snowfork <hello@snowfork.com>
|
||||
//! Converts messages from Ethereum to XCM messages
|
||||
|
||||
use crate::{v2::IGatewayV2::Payload, Log};
|
||||
use alloy_core::{
|
||||
primitives::B256,
|
||||
sol,
|
||||
sol_types::{SolEvent, SolType},
|
||||
};
|
||||
use codec::{Decode, Encode};
|
||||
use pezsp_core::{RuntimeDebug, H160, H256};
|
||||
use pezsp_std::prelude::*;
|
||||
use scale_info::TypeInfo;
|
||||
|
||||
sol! {
|
||||
interface IGatewayV2 {
|
||||
struct AsNativeTokenERC20 {
|
||||
address token_id;
|
||||
uint128 value;
|
||||
}
|
||||
struct AsForeignTokenERC20 {
|
||||
bytes32 token_id;
|
||||
uint128 value;
|
||||
}
|
||||
struct EthereumAsset {
|
||||
uint8 kind;
|
||||
bytes data;
|
||||
}
|
||||
struct Xcm {
|
||||
uint8 kind;
|
||||
bytes data;
|
||||
}
|
||||
struct XcmCreateAsset {
|
||||
address token;
|
||||
uint8 network;
|
||||
}
|
||||
struct Payload {
|
||||
address origin;
|
||||
EthereumAsset[] assets;
|
||||
Xcm xcm;
|
||||
bytes claimer;
|
||||
uint128 value;
|
||||
uint128 executionFee;
|
||||
uint128 relayerFee;
|
||||
}
|
||||
event OutboundMessageAccepted(uint64 nonce, Payload payload);
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for IGatewayV2::Payload {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("Payload")
|
||||
.field("origin", &self.origin)
|
||||
.field("assets", &self.assets)
|
||||
.field("xcm", &self.xcm)
|
||||
.field("claimer", &self.claimer)
|
||||
.field("value", &self.value)
|
||||
.field("executionFee", &self.executionFee)
|
||||
.field("relayerFee", &self.relayerFee)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for IGatewayV2::EthereumAsset {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("EthereumAsset")
|
||||
.field("kind", &self.kind)
|
||||
.field("data", &self.data)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Debug for IGatewayV2::Xcm {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
f.debug_struct("Xcm")
|
||||
.field("kind", &self.kind)
|
||||
.field("data", &self.data)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub enum XcmPayload {
|
||||
/// Represents raw XCM bytes
|
||||
Raw(Vec<u8>),
|
||||
/// A token registration template
|
||||
CreateAsset { token: H160, network: Network },
|
||||
}
|
||||
|
||||
/// Network enum for cross-chain message destination
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Encode, Decode, TypeInfo)]
|
||||
pub enum Network {
|
||||
/// Pezkuwi network
|
||||
Pezkuwi,
|
||||
}
|
||||
|
||||
/// The ethereum side sends messages which are transcoded into XCM on BH. These messages are
|
||||
/// self-contained, in that they can be transcoded using only information in the message.
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub struct Message {
|
||||
/// The address of the outbound queue on Ethereum that emitted this message as an event log
|
||||
pub gateway: H160,
|
||||
/// A nonce for enforcing replay protection and ordering.
|
||||
pub nonce: u64,
|
||||
/// The address on Ethereum that initiated the message.
|
||||
pub origin: H160,
|
||||
/// The assets sent from Ethereum (ERC-20s).
|
||||
pub assets: Vec<EthereumAsset>,
|
||||
/// The command originating from the Gateway contract.
|
||||
pub xcm: XcmPayload,
|
||||
/// The claimer in the case that funds get trapped. Expected to be an XCM::v5::Location.
|
||||
pub claimer: Option<Vec<u8>>,
|
||||
/// Native ether bridged over from Ethereum
|
||||
pub value: u128,
|
||||
/// Fee in eth to cover the xcm execution on AH.
|
||||
pub execution_fee: u128,
|
||||
/// Relayer reward in eth. Needs to cover all costs of sending a message.
|
||||
pub relayer_fee: u128,
|
||||
}
|
||||
|
||||
/// An asset that will be transacted on AH. The asset will be reserved/withdrawn and placed into
|
||||
/// the holding register. The user needs to provide additional xcm to deposit the asset
|
||||
/// in a beneficiary account.
|
||||
#[derive(Clone, Encode, Decode, RuntimeDebug, TypeInfo)]
|
||||
pub enum EthereumAsset {
|
||||
NativeTokenERC20 {
|
||||
/// The native token ID
|
||||
token_id: H160,
|
||||
/// The monetary value of the asset
|
||||
value: u128,
|
||||
},
|
||||
ForeignTokenERC20 {
|
||||
/// The foreign token ID
|
||||
token_id: H256,
|
||||
/// The monetary value of the asset
|
||||
value: u128,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, RuntimeDebug)]
|
||||
pub struct MessageDecodeError;
|
||||
|
||||
impl TryFrom<&Log> for Message {
|
||||
type Error = MessageDecodeError;
|
||||
|
||||
fn try_from(log: &Log) -> Result<Self, Self::Error> {
|
||||
// Convert to B256 for Alloy decoding
|
||||
let topics: Vec<B256> = log.topics.iter().map(|x| B256::from_slice(x.as_ref())).collect();
|
||||
|
||||
// Decode the Solidity event from raw logs
|
||||
let event = IGatewayV2::OutboundMessageAccepted::decode_raw_log_validate(topics, &log.data)
|
||||
.map_err(|_| MessageDecodeError)?;
|
||||
|
||||
let payload = event.payload;
|
||||
|
||||
let bizinikiwi_assets = Self::extract_assets(&payload)?;
|
||||
|
||||
let xcm = XcmPayload::try_from(&payload)?;
|
||||
|
||||
let mut claimer = None;
|
||||
if payload.claimer.len() > 0 {
|
||||
claimer = Some(payload.claimer.to_vec());
|
||||
}
|
||||
|
||||
let message = Message {
|
||||
gateway: log.address,
|
||||
nonce: event.nonce,
|
||||
origin: H160::from(payload.origin.as_ref()),
|
||||
assets: bizinikiwi_assets,
|
||||
xcm,
|
||||
claimer,
|
||||
value: payload.value,
|
||||
execution_fee: payload.executionFee,
|
||||
relayer_fee: payload.relayerFee,
|
||||
};
|
||||
|
||||
Ok(message)
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
fn extract_assets(
|
||||
payload: &IGatewayV2::Payload,
|
||||
) -> Result<Vec<EthereumAsset>, MessageDecodeError> {
|
||||
let mut bizinikiwi_assets = vec![];
|
||||
for asset in &payload.assets {
|
||||
bizinikiwi_assets.push(EthereumAsset::try_from(asset)?);
|
||||
}
|
||||
Ok(bizinikiwi_assets)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&IGatewayV2::Payload> for XcmPayload {
|
||||
type Error = MessageDecodeError;
|
||||
|
||||
fn try_from(payload: &Payload) -> Result<Self, Self::Error> {
|
||||
let xcm = match payload.xcm.kind {
|
||||
0 => XcmPayload::Raw(payload.xcm.data.to_vec()),
|
||||
1 => {
|
||||
let create_asset =
|
||||
IGatewayV2::XcmCreateAsset::abi_decode_validate(&payload.xcm.data)
|
||||
.map_err(|_| MessageDecodeError)?;
|
||||
// Convert u8 network to Network enum
|
||||
let network = match create_asset.network {
|
||||
0 => Network::Pezkuwi,
|
||||
_ => return Err(MessageDecodeError),
|
||||
};
|
||||
XcmPayload::CreateAsset { token: H160::from(create_asset.token.as_ref()), network }
|
||||
},
|
||||
_ => return Err(MessageDecodeError),
|
||||
};
|
||||
Ok(xcm)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<&IGatewayV2::EthereumAsset> for EthereumAsset {
|
||||
type Error = MessageDecodeError;
|
||||
|
||||
fn try_from(asset: &IGatewayV2::EthereumAsset) -> Result<EthereumAsset, Self::Error> {
|
||||
let asset = match asset.kind {
|
||||
0 => {
|
||||
let native_data = IGatewayV2::AsNativeTokenERC20::abi_decode_validate(&asset.data)
|
||||
.map_err(|_| MessageDecodeError)?;
|
||||
EthereumAsset::NativeTokenERC20 {
|
||||
token_id: H160::from(native_data.token_id.as_ref()),
|
||||
value: native_data.value,
|
||||
}
|
||||
},
|
||||
1 => {
|
||||
let foreign_data =
|
||||
IGatewayV2::AsForeignTokenERC20::abi_decode_validate(&asset.data)
|
||||
.map_err(|_| MessageDecodeError)?;
|
||||
EthereumAsset::ForeignTokenERC20 {
|
||||
token_id: H256::from(foreign_data.token_id.as_ref()),
|
||||
value: foreign_data.value,
|
||||
}
|
||||
},
|
||||
_ => return Err(MessageDecodeError),
|
||||
};
|
||||
Ok(asset)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
use pezframe_support::assert_ok;
|
||||
use pezsp_core::H160;
|
||||
|
||||
#[test]
|
||||
fn test_decode() {
|
||||
let log = Log{
|
||||
address: hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305").into(),
|
||||
topics: vec![hex!("550e2067494b1736ea5573f2d19cdc0ac95b410fff161bf16f11c6229655ec9c").into()],
|
||||
data: hex!("00000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b1185ede04202fe62d38f5db72f71e38ff3e830500000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001a0000000000000000000000000000000000000000000000000000009184e72a0000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000015d3ef798000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000040000000000000000000000000b8ea8cb425d85536b158d661da1ef0895bb92f1d00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").to_vec(),
|
||||
};
|
||||
|
||||
let result = Message::try_from(&log);
|
||||
assert_ok!(result.clone());
|
||||
let message = result.unwrap();
|
||||
|
||||
assert_eq!(H160::from(hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305")), message.gateway);
|
||||
assert_eq!(H160::from(hex!("b1185ede04202fe62d38f5db72f71e38ff3e8305")), message.origin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2025 Snowfork <hello@snowfork.com>
|
||||
// SPDX-FileCopyrightText: 2021-2025 Parity Technologies (UK) Ltd.
|
||||
|
||||
pub mod converter;
|
||||
pub mod message;
|
||||
pub mod traits;
|
||||
|
||||
pub use converter::*;
|
||||
pub use message::*;
|
||||
pub use traits::*;
|
||||
|
||||
const LOG_TARGET: &str = "pezsnowbridge-inbound-queue-primitives";
|
||||
@@ -0,0 +1,23 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: 2025 Snowfork <hello@snowfork.com>
|
||||
// SPDX-FileCopyrightText: 2021-2025 Parity Technologies (UK) Ltd.
|
||||
use super::Message;
|
||||
use pezsp_core::RuntimeDebug;
|
||||
use xcm::latest::Xcm;
|
||||
|
||||
/// Converts an inbound message from Ethereum to an XCM message that can be
|
||||
/// executed on a teyrchain.
|
||||
pub trait ConvertMessage {
|
||||
fn convert(message: Message) -> Result<Xcm<()>, ConvertMessageError>;
|
||||
}
|
||||
|
||||
/// Reason why a message conversion failed.
|
||||
#[derive(Copy, Clone, RuntimeDebug, PartialEq)]
|
||||
pub enum ConvertMessageError {
|
||||
/// Invalid foreign ERC-20 token ID
|
||||
InvalidAsset,
|
||||
/// Cannot reachor a foreign ERC-20 asset location.
|
||||
CannotReanchor,
|
||||
/// Invalid network specified (not from Ethereum)
|
||||
InvalidNetwork,
|
||||
}
|
||||
Reference in New Issue
Block a user