Skip to content

Commit

Permalink
Transfer Polkadot-native assets to Ethereum (paritytech#5546)
Browse files Browse the repository at this point in the history
# Description

Adding support for send polkadot native assets(PNA) to Ethereum network
through snowbridge. Asset with location in view of AH Including:

- Relay token `(1,Here)`
- Native asset `(0,[PalletInstance(instance),GenereIndex(index)])`
managed by Assets Pallet
- Native asset of Parachain `(1,[Parachain(paraId)])` managed by Foreign
Assets Pallet

The original PR in Snowfork#128
which has been internally reviewed by Snowbridge team.

# Notes

- This feature depends on the companion solidity change in
Snowfork/snowbridge#1155. Currently register PNA
is only allowed from
[sudo](https://github.com/Snowfork/polkadot-sdk/blob/46cb3528cd8cd1394af2335a6907d7ab8647717a/bridges/snowbridge/pallets/system/src/lib.rs#L621),
so it's actually not enabled. Will require another runtime upgrade to
make the call permissionless together with upgrading the Gateway
contract.

- To make things easy multi-hop transfer(i.e. sending PNA from Ethereum
through AH to Destination chain) is not support ed in this PR. For this
case user can switch to 2-phases transfer instead.

---------

Co-authored-by: Clara van Staden <claravanstaden64@gmail.com>
Co-authored-by: Alistair Singh <alistair.singh7@gmail.com>
Co-authored-by: Vincent Geddes <117534+vgeddes@users.noreply.github.com>
Co-authored-by: Francisco Aguirre <franciscoaguirreperez@gmail.com>
Co-authored-by: Adrian Catangiu <adrian@parity.io>
  • Loading branch information
6 people authored Sep 13, 2024
1 parent 0136463 commit fb7300c
Show file tree
Hide file tree
Showing 29 changed files with 1,626 additions and 298 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

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

29 changes: 13 additions & 16 deletions bridges/snowbridge/pallets/inbound-queue/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,11 @@ use frame_support::{
};
use frame_system::ensure_signed;
use scale_info::TypeInfo;
use sp_core::{H160, H256};
use sp_core::H160;
use sp_runtime::traits::Zero;
use sp_std::vec;
use xcm::prelude::{
send_xcm, Instruction::SetTopic, Junction::*, Location, SendError as XcmpSendError, SendXcm,
Xcm, XcmContext, XcmHash,
send_xcm, Junction::*, Location, SendError as XcmpSendError, SendXcm, Xcm, XcmContext, XcmHash,
};
use xcm_executor::traits::TransactAsset;

Expand All @@ -62,9 +61,8 @@ use snowbridge_core::{
sibling_sovereign_account, BasicOperatingMode, Channel, ChannelId, ParaId, PricingParameters,
StaticLookup,
};
use snowbridge_router_primitives::{
inbound,
inbound::{ConvertMessage, ConvertMessageError},
use snowbridge_router_primitives::inbound::{
ConvertMessage, ConvertMessageError, VersionedMessage,
};
use sp_runtime::{traits::Saturating, SaturatedConversion, TokenError};

Expand All @@ -86,6 +84,7 @@ pub mod pallet {

use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
use sp_core::H256;

#[pallet::pallet]
pub struct Pallet<T>(_);
Expand Down Expand Up @@ -276,12 +275,12 @@ pub mod pallet {
T::Token::transfer(&sovereign_account, &who, amount, Preservation::Preserve)?;
}

// Decode payload into `VersionedMessage`
let message = VersionedMessage::decode_all(&mut envelope.payload.as_ref())
.map_err(|_| Error::<T>::InvalidPayload)?;

// Decode message into XCM
let (xcm, fee) =
match inbound::VersionedMessage::decode_all(&mut envelope.payload.as_ref()) {
Ok(message) => Self::do_convert(envelope.message_id, message)?,
Err(_) => return Err(Error::<T>::InvalidPayload.into()),
};
let (xcm, fee) = Self::do_convert(envelope.message_id, message.clone())?;

log::info!(
target: LOG_TARGET,
Expand Down Expand Up @@ -323,12 +322,10 @@ pub mod pallet {
impl<T: Config> Pallet<T> {
pub fn do_convert(
message_id: H256,
message: inbound::VersionedMessage,
message: VersionedMessage,
) -> Result<(Xcm<()>, BalanceOf<T>), Error<T>> {
let (mut xcm, fee) =
T::MessageConverter::convert(message).map_err(|e| Error::<T>::ConvertMessage(e))?;
// Append the message id as an XCM topic
xcm.inner_mut().extend(vec![SetTopic(message_id.into())]);
let (xcm, fee) = T::MessageConverter::convert(message_id, message)
.map_err(|e| Error::<T>::ConvertMessage(e))?;
Ok((xcm, fee))
}

Expand Down
20 changes: 18 additions & 2 deletions bridges/snowbridge/pallets/inbound-queue/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ use snowbridge_beacon_primitives::{
use snowbridge_core::{
gwei,
inbound::{Log, Proof, VerificationError},
meth, Channel, ChannelId, PricingParameters, Rewards, StaticLookup,
meth, Channel, ChannelId, PricingParameters, Rewards, StaticLookup, TokenId,
};
use snowbridge_router_primitives::inbound::MessageToXcm;
use sp_core::{H160, H256};
use sp_runtime::{
traits::{IdentifyAccount, IdentityLookup, Verify},
traits::{IdentifyAccount, IdentityLookup, MaybeEquivalence, Verify},
BuildStorage, FixedU128, MultiSignature,
};
use sp_std::{convert::From, default::Default};
Expand Down Expand Up @@ -112,6 +112,9 @@ parameter_types! {
pub const SendTokenExecutionFee: u128 = 1_000_000_000;
pub const InitialFund: u128 = 1_000_000_000_000;
pub const InboundQueuePalletInstance: u8 = 80;
pub UniversalLocation: InteriorLocation =
[GlobalConsensus(Westend), Parachain(1002)].into();
pub AssetHubFromEthereum: Location = Location::new(1,[GlobalConsensus(Westend),Parachain(1000)]);
}

#[cfg(feature = "runtime-benchmarks")]
Expand Down Expand Up @@ -205,6 +208,16 @@ impl TransactAsset for SuccessfulTransactor {
}
}

pub struct MockTokenIdConvert;
impl MaybeEquivalence<TokenId, Location> for MockTokenIdConvert {
fn convert(_id: &TokenId) -> Option<Location> {
Some(Location::parent())
}
fn convert_back(_loc: &Location) -> Option<TokenId> {
None
}
}

impl inbound_queue::Config for Test {
type RuntimeEvent = RuntimeEvent;
type Verifier = MockVerifier;
Expand All @@ -218,6 +231,9 @@ impl inbound_queue::Config for Test {
InboundQueuePalletInstance,
AccountId,
Balance,
MockTokenIdConvert,
UniversalLocation,
AssetHubFromEthereum,
>;
type PricingParameters = Parameters;
type ChannelLookup = MockChannelLookup;
Expand Down
10 changes: 4 additions & 6 deletions bridges/snowbridge/pallets/outbound-queue/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,13 +164,11 @@ pub fn mock_message(sibling_para_id: u32) -> Message {
Message {
id: None,
channel_id: ParaId::from(sibling_para_id).into(),
command: Command::AgentExecute {
command: Command::TransferNativeToken {
agent_id: Default::default(),
command: AgentExecuteCommand::TransferToken {
token: Default::default(),
recipient: Default::default(),
amount: 0,
},
token: Default::default(),
recipient: Default::default(),
amount: 0,
},
}
}
23 changes: 23 additions & 0 deletions bridges/snowbridge/pallets/system/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,29 @@ mod benchmarks {
Ok(())
}

#[benchmark]
fn register_token() -> Result<(), BenchmarkError> {
let caller: T::AccountId = whitelisted_caller();

let amount: BalanceOf<T> =
(10_000_000_000_000_u128).saturated_into::<u128>().saturated_into();

T::Token::mint_into(&caller, amount)?;

let relay_token_asset_id: Location = Location::parent();
let asset = Box::new(VersionedLocation::V4(relay_token_asset_id));
let asset_metadata = AssetMetadata {
name: "wnd".as_bytes().to_vec().try_into().unwrap(),
symbol: "wnd".as_bytes().to_vec().try_into().unwrap(),
decimals: 12,
};

#[extrinsic_call]
_(RawOrigin::Root, asset, asset_metadata);

Ok(())
}

impl_benchmark_test_suite!(
SnowbridgeControl,
crate::mock::new_test_ext(true),
Expand Down
116 changes: 111 additions & 5 deletions bridges/snowbridge/pallets/system/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@
//!
//! Typically, Polkadot governance will use the `force_transfer_native_from_agent` and
//! `force_update_channel` and extrinsics to manage agents and channels for system parachains.
//!
//! ## Polkadot-native tokens on Ethereum
//!
//! Tokens deposited on AssetHub pallet can be bridged to Ethereum as wrapped ERC20 tokens. As a
//! prerequisite, the token should be registered first.
//!
//! * [`Call::register_token`]: Register a token location as a wrapped ERC20 contract on Ethereum.
#![cfg_attr(not(feature = "std"), no_std)]

#[cfg(test)]
mod mock;

Expand All @@ -63,13 +69,16 @@ use frame_system::pallet_prelude::*;
use snowbridge_core::{
meth,
outbound::{Command, Initializer, Message, OperatingMode, SendError, SendMessage},
sibling_sovereign_account, AgentId, Channel, ChannelId, ParaId,
PricingParameters as PricingParametersRecord, PRIMARY_GOVERNANCE_CHANNEL,
sibling_sovereign_account, AgentId, AssetMetadata, Channel, ChannelId, ParaId,
PricingParameters as PricingParametersRecord, TokenId, TokenIdOf, PRIMARY_GOVERNANCE_CHANNEL,
SECONDARY_GOVERNANCE_CHANNEL,
};
use sp_core::{RuntimeDebug, H160, H256};
use sp_io::hashing::blake2_256;
use sp_runtime::{traits::BadOrigin, DispatchError, SaturatedConversion};
use sp_runtime::{
traits::{BadOrigin, MaybeEquivalence},
DispatchError, SaturatedConversion,
};
use sp_std::prelude::*;
use xcm::prelude::*;
use xcm_executor::traits::ConvertLocation;
Expand Down Expand Up @@ -99,7 +108,7 @@ where
}

/// Hash the location to produce an agent id
fn agent_id_of<T: Config>(location: &Location) -> Result<H256, DispatchError> {
pub fn agent_id_of<T: Config>(location: &Location) -> Result<H256, DispatchError> {
T::AgentIdOf::convert_location(location).ok_or(Error::<T>::LocationConversionFailed.into())
}

Expand Down Expand Up @@ -127,6 +136,7 @@ where

#[frame_support::pallet]
pub mod pallet {
use frame_support::dispatch::PostDispatchInfo;
use snowbridge_core::StaticLookup;
use sp_core::U256;

Expand Down Expand Up @@ -164,6 +174,12 @@ pub mod pallet {

type WeightInfo: WeightInfo;

/// This chain's Universal Location.
type UniversalLocation: Get<InteriorLocation>;

// The bridges configured Ethereum location
type EthereumLocation: Get<Location>;

#[cfg(feature = "runtime-benchmarks")]
type Helper: BenchmarkHelper<Self::RuntimeOrigin>;
}
Expand Down Expand Up @@ -211,6 +227,13 @@ pub mod pallet {
PricingParametersChanged {
params: PricingParametersOf<T>,
},
/// Register Polkadot-native token as a wrapped ERC20 token on Ethereum
RegisterToken {
/// Location of Polkadot-native token
location: VersionedLocation,
/// ID of Polkadot-native token on Ethereum
foreign_token_id: H256,
},
}

#[pallet::error]
Expand Down Expand Up @@ -243,6 +266,16 @@ pub mod pallet {
pub type PricingParameters<T: Config> =
StorageValue<_, PricingParametersOf<T>, ValueQuery, T::DefaultPricingParameters>;

/// Lookup table for foreign token ID to native location relative to ethereum
#[pallet::storage]
pub type ForeignToNativeId<T: Config> =
StorageMap<_, Blake2_128Concat, TokenId, xcm::v4::Location, OptionQuery>;

/// Lookup table for native location relative to ethereum to foreign token ID
#[pallet::storage]
pub type NativeToForeignId<T: Config> =
StorageMap<_, Blake2_128Concat, xcm::v4::Location, TokenId, OptionQuery>;

#[pallet::genesis_config]
#[derive(frame_support::DefaultNoBound)]
pub struct GenesisConfig<T: Config> {
Expand Down Expand Up @@ -574,6 +607,34 @@ pub mod pallet {
});
Ok(())
}

/// Registers a Polkadot-native token as a wrapped ERC20 token on Ethereum.
/// Privileged. Can only be called by root.
///
/// Fee required: No
///
/// - `origin`: Must be root
/// - `location`: Location of the asset (relative to this chain)
/// - `metadata`: Metadata to include in the instantiated ERC20 contract on Ethereum
#[pallet::call_index(10)]
#[pallet::weight(T::WeightInfo::register_token())]
pub fn register_token(
origin: OriginFor<T>,
location: Box<VersionedLocation>,
metadata: AssetMetadata,
) -> DispatchResultWithPostInfo {
ensure_root(origin)?;

let location: Location =
(*location).try_into().map_err(|_| Error::<T>::UnsupportedLocationVersion)?;

Self::do_register_token(&location, metadata, PaysFee::<T>::No)?;

Ok(PostDispatchInfo {
actual_weight: Some(T::WeightInfo::register_token()),
pays_fee: Pays::No,
})
}
}

impl<T: Config> Pallet<T> {
Expand Down Expand Up @@ -663,6 +724,42 @@ pub mod pallet {
let secondary_exists = Channels::<T>::contains_key(SECONDARY_GOVERNANCE_CHANNEL);
primary_exists && secondary_exists
}

pub(crate) fn do_register_token(
location: &Location,
metadata: AssetMetadata,
pays_fee: PaysFee<T>,
) -> Result<(), DispatchError> {
let ethereum_location = T::EthereumLocation::get();
// reanchor to Ethereum context
let location = location
.clone()
.reanchored(&ethereum_location, &T::UniversalLocation::get())
.map_err(|_| Error::<T>::LocationConversionFailed)?;

let token_id = TokenIdOf::convert_location(&location)
.ok_or(Error::<T>::LocationConversionFailed)?;

if !ForeignToNativeId::<T>::contains_key(token_id) {
NativeToForeignId::<T>::insert(location.clone(), token_id);
ForeignToNativeId::<T>::insert(token_id, location.clone());
}

let command = Command::RegisterForeignToken {
token_id,
name: metadata.name.into_inner(),
symbol: metadata.symbol.into_inner(),
decimals: metadata.decimals,
};
Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?;

Self::deposit_event(Event::<T>::RegisterToken {
location: location.clone().into(),
foreign_token_id: token_id,
});

Ok(())
}
}

impl<T: Config> StaticLookup for Pallet<T> {
Expand All @@ -684,4 +781,13 @@ pub mod pallet {
PricingParameters::<T>::get()
}
}

impl<T: Config> MaybeEquivalence<TokenId, Location> for Pallet<T> {
fn convert(foreign_id: &TokenId) -> Option<Location> {
ForeignToNativeId::<T>::get(foreign_id)
}
fn convert_back(location: &Location) -> Option<TokenId> {
NativeToForeignId::<T>::get(location)
}
}
}
Loading

0 comments on commit fb7300c

Please sign in to comment.