// SPDX-License-Identifier: Apache-2.0 // SPDX-FileCopyrightText: 2023 Snowfork //! Governance API for controlling the Ethereum side of the bridge //! //! # Extrinsics //! //! ## Agents //! //! Agents are smart contracts on Ethereum that act as proxies for consensus systems on Polkadot //! networks. //! //! * [`Call::create_agent`]: Create agent for a sibling parachain //! * [`Call::transfer_native_from_agent`]: Withdraw ether from an agent //! //! The `create_agent` extrinsic should be called via an XCM `Transact` instruction from the sibling //! parachain. //! //! ## Channels //! //! Each sibling parachain has its own dedicated messaging channel for sending and receiving //! messages. As a prerequisite to creating a channel, the sibling should have already created //! an agent using the `create_agent` extrinsic. //! //! * [`Call::create_channel`]: Create channel for a sibling //! * [`Call::update_channel`]: Update a channel for a sibling //! //! ## Governance //! //! Only Polkadot governance itself can call these extrinsics. Delivery fees are waived. //! //! * [`Call::upgrade`]`: Upgrade the gateway contract //! * [`Call::set_operating_mode`]: Update the operating mode of the gateway contract //! * [`Call::force_update_channel`]: Allow root to update a channel for a sibling //! * [`Call::force_transfer_native_from_agent`]: Allow root to withdraw ether from an agent //! //! 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. #![cfg_attr(not(feature = "std"), no_std)] pub use pallet::*; #[cfg(test)] mod mock; #[cfg(test)] mod tests; #[cfg(feature = "runtime-benchmarks")] mod benchmarking; pub mod migration; pub mod api; pub mod weights; pub use weights::*; use frame_support::{ pallet_prelude::*, traits::{ fungible::{Inspect, Mutate}, tokens::Preservation, Contains, EnsureOrigin, }, }; 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, SECONDARY_GOVERNANCE_CHANNEL, }; use sp_core::{RuntimeDebug, H160, H256}; use sp_io::hashing::blake2_256; use sp_runtime::{traits::BadOrigin, DispatchError, SaturatedConversion}; use sp_std::prelude::*; use xcm::prelude::*; use xcm_executor::traits::ConvertLocation; #[cfg(feature = "runtime-benchmarks")] use frame_support::traits::OriginTrait; pub use pallet::*; pub type BalanceOf = <::Token as Inspect<::AccountId>>::Balance; pub type AccountIdOf = ::AccountId; pub type PricingParametersOf = PricingParametersRecord>; /// Ensure origin location is a sibling fn ensure_sibling(location: &Location) -> Result<(ParaId, H256), DispatchError> where T: Config, { match location.unpack() { (1, [Parachain(para_id)]) => { let agent_id = agent_id_of::(location)?; Ok(((*para_id).into(), agent_id)) }, _ => Err(BadOrigin.into()), } } /// Hash the location to produce an agent id fn agent_id_of(location: &Location) -> Result { T::AgentIdOf::convert_location(location).ok_or(Error::::LocationConversionFailed.into()) } #[cfg(feature = "runtime-benchmarks")] pub trait BenchmarkHelper where O: OriginTrait, { fn make_xcm_origin(location: Location) -> O; } /// Whether a fee should be withdrawn to an account for sending an outbound message #[derive(Clone, PartialEq, RuntimeDebug)] pub enum PaysFee where T: Config, { /// Fully charge includes (local + remote fee) Yes(AccountIdOf), /// Partially charge includes local fee only Partial(AccountIdOf), /// No charge No, } #[frame_support::pallet] pub mod pallet { use snowbridge_core::StaticLookup; use sp_core::U256; use super::*; #[pallet::pallet] pub struct Pallet(_); #[pallet::config] pub trait Config: frame_system::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Send messages to Ethereum type OutboundQueue: SendMessage>; /// Origin check for XCM locations that can create agents type SiblingOrigin: EnsureOrigin; /// Converts Location to AgentId type AgentIdOf: ConvertLocation; /// Token reserved for control operations type Token: Mutate; /// TreasuryAccount to collect fees #[pallet::constant] type TreasuryAccount: Get; /// Number of decimal places of local currency type DefaultPricingParameters: Get>; /// Cost of delivering a message from Ethereum type InboundDeliveryCost: Get>; type WeightInfo: WeightInfo; #[cfg(feature = "runtime-benchmarks")] type Helper: BenchmarkHelper; } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// An Upgrade message was sent to the Gateway Upgrade { impl_address: H160, impl_code_hash: H256, initializer_params_hash: Option, }, /// An CreateAgent message was sent to the Gateway CreateAgent { location: Box, agent_id: AgentId, }, /// An CreateChannel message was sent to the Gateway CreateChannel { channel_id: ChannelId, agent_id: AgentId, }, /// An UpdateChannel message was sent to the Gateway UpdateChannel { channel_id: ChannelId, mode: OperatingMode, }, /// An SetOperatingMode message was sent to the Gateway SetOperatingMode { mode: OperatingMode, }, /// An TransferNativeFromAgent message was sent to the Gateway TransferNativeFromAgent { agent_id: AgentId, recipient: H160, amount: u128, }, /// A SetTokenTransferFees message was sent to the Gateway SetTokenTransferFees { create_asset_xcm: u128, transfer_asset_xcm: u128, register_token: U256, }, PricingParametersChanged { params: PricingParametersOf, }, } #[pallet::error] pub enum Error { LocationConversionFailed, AgentAlreadyCreated, NoAgent, ChannelAlreadyCreated, NoChannel, UnsupportedLocationVersion, InvalidLocation, Send(SendError), InvalidTokenTransferFees, InvalidPricingParameters, } /// The set of registered agents #[pallet::storage] #[pallet::getter(fn agents)] pub type Agents = StorageMap<_, Twox64Concat, AgentId, (), OptionQuery>; /// The set of registered channels #[pallet::storage] #[pallet::getter(fn channels)] pub type Channels = StorageMap<_, Twox64Concat, ChannelId, Channel, OptionQuery>; #[pallet::storage] #[pallet::getter(fn parameters)] pub type PricingParameters = StorageValue<_, PricingParametersOf, ValueQuery, T::DefaultPricingParameters>; #[pallet::genesis_config] #[derive(frame_support::DefaultNoBound)] pub struct GenesisConfig { // Own parachain id pub para_id: ParaId, // AssetHub's parachain id pub asset_hub_para_id: ParaId, #[serde(skip)] pub _config: PhantomData, } #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { Pallet::::initialize(self.para_id, self.asset_hub_para_id).expect("infallible; qed"); } } #[pallet::call] impl Pallet { /// Sends command to the Gateway contract to upgrade itself with a new implementation /// contract /// /// Fee required: No /// /// - `origin`: Must be `Root`. /// - `impl_address`: The address of the implementation contract. /// - `impl_code_hash`: The codehash of the implementation contract. /// - `initializer`: Optionally call an initializer on the implementation contract. #[pallet::call_index(0)] #[pallet::weight((T::WeightInfo::upgrade(), DispatchClass::Operational))] pub fn upgrade( origin: OriginFor, impl_address: H160, impl_code_hash: H256, initializer: Option, ) -> DispatchResult { ensure_root(origin)?; let initializer_params_hash: Option = initializer.as_ref().map(|i| H256::from(blake2_256(i.params.as_ref()))); let command = Command::Upgrade { impl_address, impl_code_hash, initializer }; Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::::No)?; Self::deposit_event(Event::::Upgrade { impl_address, impl_code_hash, initializer_params_hash, }); Ok(()) } /// Sends a message to the Gateway contract to change its operating mode /// /// Fee required: No /// /// - `origin`: Must be `Location` #[pallet::call_index(1)] #[pallet::weight((T::WeightInfo::set_operating_mode(), DispatchClass::Operational))] pub fn set_operating_mode(origin: OriginFor, mode: OperatingMode) -> DispatchResult { ensure_root(origin)?; let command = Command::SetOperatingMode { mode }; Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::::No)?; Self::deposit_event(Event::::SetOperatingMode { mode }); Ok(()) } /// Set pricing parameters on both sides of the bridge /// /// Fee required: No /// /// - `origin`: Must be root #[pallet::call_index(2)] #[pallet::weight((T::WeightInfo::set_pricing_parameters(), DispatchClass::Operational))] pub fn set_pricing_parameters( origin: OriginFor, params: PricingParametersOf, ) -> DispatchResult { ensure_root(origin)?; params.validate().map_err(|_| Error::::InvalidPricingParameters)?; PricingParameters::::put(params.clone()); let command = Command::SetPricingParameters { exchange_rate: params.exchange_rate.into(), delivery_cost: T::InboundDeliveryCost::get().saturated_into::(), }; Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::::No)?; Self::deposit_event(Event::PricingParametersChanged { params }); Ok(()) } /// Sends a command to the Gateway contract to instantiate a new agent contract representing /// `origin`. /// /// Fee required: Yes /// /// - `origin`: Must be `Location` of a sibling parachain #[pallet::call_index(3)] #[pallet::weight(T::WeightInfo::create_agent())] pub fn create_agent(origin: OriginFor) -> DispatchResult { let origin_location: Location = T::SiblingOrigin::ensure_origin(origin)?; // Ensure that origin location is some consensus system on a sibling parachain let (para_id, agent_id) = ensure_sibling::(&origin_location)?; // Record the agent id or fail if it has already been created ensure!(!Agents::::contains_key(agent_id), Error::::AgentAlreadyCreated); Agents::::insert(agent_id, ()); let command = Command::CreateAgent { agent_id }; let pays_fee = PaysFee::::Yes(sibling_sovereign_account::(para_id)); Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?; Self::deposit_event(Event::::CreateAgent { location: Box::new(origin_location), agent_id, }); Ok(()) } /// Sends a message to the Gateway contract to create a new channel representing `origin` /// /// Fee required: Yes /// /// This extrinsic is permissionless, so a fee is charged to prevent spamming and pay /// for execution costs on the remote side. /// /// The message is sent over the bridge on BridgeHub's own channel to the Gateway. /// /// - `origin`: Must be `Location` /// - `mode`: Initial operating mode of the channel #[pallet::call_index(4)] #[pallet::weight(T::WeightInfo::create_channel())] pub fn create_channel(origin: OriginFor, mode: OperatingMode) -> DispatchResult { let origin_location: Location = T::SiblingOrigin::ensure_origin(origin)?; // Ensure that origin location is a sibling parachain let (para_id, agent_id) = ensure_sibling::(&origin_location)?; let channel_id: ChannelId = para_id.into(); ensure!(Agents::::contains_key(agent_id), Error::::NoAgent); ensure!(!Channels::::contains_key(channel_id), Error::::ChannelAlreadyCreated); let channel = Channel { agent_id, para_id }; Channels::::insert(channel_id, channel); let command = Command::CreateChannel { channel_id, agent_id, mode }; let pays_fee = PaysFee::::Yes(sibling_sovereign_account::(para_id)); Self::send(SECONDARY_GOVERNANCE_CHANNEL, command, pays_fee)?; Self::deposit_event(Event::::CreateChannel { channel_id, agent_id }); Ok(()) } /// Sends a message to the Gateway contract to update a channel configuration /// /// The origin must already have a channel initialized, as this message is sent over it. /// /// A partial fee will be charged for local processing only. /// /// - `origin`: Must be `Location` /// - `mode`: Initial operating mode of the channel #[pallet::call_index(5)] #[pallet::weight(T::WeightInfo::update_channel())] pub fn update_channel(origin: OriginFor, mode: OperatingMode) -> DispatchResult { let origin_location: Location = T::SiblingOrigin::ensure_origin(origin)?; // Ensure that origin location is a sibling parachain let (para_id, _) = ensure_sibling::(&origin_location)?; let channel_id: ChannelId = para_id.into(); ensure!(Channels::::contains_key(channel_id), Error::::NoChannel); let command = Command::UpdateChannel { channel_id, mode }; let pays_fee = PaysFee::::Partial(sibling_sovereign_account::(para_id)); // Parachains send the update message on their own channel Self::send(channel_id, command, pays_fee)?; Self::deposit_event(Event::::UpdateChannel { channel_id, mode }); Ok(()) } /// Sends a message to the Gateway contract to update an arbitrary channel /// /// Fee required: No /// /// - `origin`: Must be root /// - `channel_id`: ID of channel /// - `mode`: Initial operating mode of the channel /// - `outbound_fee`: Fee charged to users for sending outbound messages to Polkadot #[pallet::call_index(6)] #[pallet::weight(T::WeightInfo::force_update_channel())] pub fn force_update_channel( origin: OriginFor, channel_id: ChannelId, mode: OperatingMode, ) -> DispatchResult { ensure_root(origin)?; ensure!(Channels::::contains_key(channel_id), Error::::NoChannel); let command = Command::UpdateChannel { channel_id, mode }; Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::::No)?; Self::deposit_event(Event::::UpdateChannel { channel_id, mode }); Ok(()) } /// Sends a message to the Gateway contract to transfer ether from an agent to `recipient`. /// /// A partial fee will be charged for local processing only. /// /// - `origin`: Must be `Location` #[pallet::call_index(7)] #[pallet::weight(T::WeightInfo::transfer_native_from_agent())] pub fn transfer_native_from_agent( origin: OriginFor, recipient: H160, amount: u128, ) -> DispatchResult { let origin_location: Location = T::SiblingOrigin::ensure_origin(origin)?; // Ensure that origin location is some consensus system on a sibling parachain let (para_id, agent_id) = ensure_sibling::(&origin_location)?; // Since the origin is also the owner of the channel, they only need to pay // the local processing fee. let pays_fee = PaysFee::::Partial(sibling_sovereign_account::(para_id)); Self::do_transfer_native_from_agent( agent_id, para_id.into(), recipient, amount, pays_fee, ) } /// Sends a message to the Gateway contract to transfer ether from an agent to `recipient`. /// /// Privileged. Can only be called by root. /// /// Fee required: No /// /// - `origin`: Must be root /// - `location`: Location used to resolve the agent /// - `recipient`: Recipient of funds /// - `amount`: Amount to transfer #[pallet::call_index(8)] #[pallet::weight(T::WeightInfo::force_transfer_native_from_agent())] pub fn force_transfer_native_from_agent( origin: OriginFor, location: Box, recipient: H160, amount: u128, ) -> DispatchResult { ensure_root(origin)?; // Ensure that location is some consensus system on a sibling parachain let location: Location = (*location).try_into().map_err(|_| Error::::UnsupportedLocationVersion)?; let (_, agent_id) = ensure_sibling::(&location).map_err(|_| Error::::InvalidLocation)?; let pays_fee = PaysFee::::No; Self::do_transfer_native_from_agent( agent_id, PRIMARY_GOVERNANCE_CHANNEL, recipient, amount, pays_fee, ) } /// Sends a message to the Gateway contract to update fee related parameters for /// token transfers. /// /// Privileged. Can only be called by root. /// /// Fee required: No /// /// - `origin`: Must be root /// - `create_asset_xcm`: The XCM execution cost for creating a new asset class on AssetHub, /// in DOT /// - `transfer_asset_xcm`: The XCM execution cost for performing a reserve transfer on /// AssetHub, in DOT /// - `register_token`: The Ether fee for registering a new token, to discourage spamming #[pallet::call_index(9)] #[pallet::weight((T::WeightInfo::set_token_transfer_fees(), DispatchClass::Operational))] pub fn set_token_transfer_fees( origin: OriginFor, create_asset_xcm: u128, transfer_asset_xcm: u128, register_token: U256, ) -> DispatchResult { ensure_root(origin)?; // Basic validation of new costs. Particularly for token registration, we want to ensure // its relatively expensive to discourage spamming. Like at least 100 USD. ensure!( create_asset_xcm > 0 && transfer_asset_xcm > 0 && register_token > meth(100), Error::::InvalidTokenTransferFees ); let command = Command::SetTokenTransferFees { create_asset_xcm, transfer_asset_xcm, register_token, }; Self::send(PRIMARY_GOVERNANCE_CHANNEL, command, PaysFee::::No)?; Self::deposit_event(Event::::SetTokenTransferFees { create_asset_xcm, transfer_asset_xcm, register_token, }); Ok(()) } } impl Pallet { /// Send `command` to the Gateway on the Channel identified by `channel_id` fn send(channel_id: ChannelId, command: Command, pays_fee: PaysFee) -> DispatchResult { let message = Message { id: None, channel_id, command }; let (ticket, fee) = T::OutboundQueue::validate(&message).map_err(|err| Error::::Send(err))?; let payment = match pays_fee { PaysFee::Yes(account) => Some((account, fee.total())), PaysFee::Partial(account) => Some((account, fee.local)), PaysFee::No => None, }; if let Some((payer, fee)) = payment { T::Token::transfer( &payer, &T::TreasuryAccount::get(), fee, Preservation::Preserve, )?; } T::OutboundQueue::deliver(ticket).map_err(|err| Error::::Send(err))?; Ok(()) } /// Issue a `Command::TransferNativeFromAgent` command. The command will be sent on the /// channel `channel_id` pub fn do_transfer_native_from_agent( agent_id: H256, channel_id: ChannelId, recipient: H160, amount: u128, pays_fee: PaysFee, ) -> DispatchResult { ensure!(Agents::::contains_key(agent_id), Error::::NoAgent); let command = Command::TransferNativeFromAgent { agent_id, recipient, amount }; Self::send(channel_id, command, pays_fee)?; Self::deposit_event(Event::::TransferNativeFromAgent { agent_id, recipient, amount, }); Ok(()) } /// Initializes agents and channels. pub fn initialize(para_id: ParaId, asset_hub_para_id: ParaId) -> Result<(), DispatchError> { // Asset Hub let asset_hub_location: Location = ParentThen(Parachain(asset_hub_para_id.into()).into()).into(); let asset_hub_agent_id = agent_id_of::(&asset_hub_location)?; let asset_hub_channel_id: ChannelId = asset_hub_para_id.into(); Agents::::insert(asset_hub_agent_id, ()); Channels::::insert( asset_hub_channel_id, Channel { agent_id: asset_hub_agent_id, para_id: asset_hub_para_id }, ); // Governance channels let bridge_hub_agent_id = agent_id_of::(&Location::here())?; // Agent for BridgeHub Agents::::insert(bridge_hub_agent_id, ()); // Primary governance channel Channels::::insert( PRIMARY_GOVERNANCE_CHANNEL, Channel { agent_id: bridge_hub_agent_id, para_id }, ); // Secondary governance channel Channels::::insert( SECONDARY_GOVERNANCE_CHANNEL, Channel { agent_id: bridge_hub_agent_id, para_id }, ); Ok(()) } /// Checks if the pallet has been initialized. pub(crate) fn is_initialized() -> bool { let primary_exists = Channels::::contains_key(PRIMARY_GOVERNANCE_CHANNEL); let secondary_exists = Channels::::contains_key(SECONDARY_GOVERNANCE_CHANNEL); primary_exists && secondary_exists } } impl StaticLookup for Pallet { type Source = ChannelId; type Target = Channel; fn lookup(channel_id: Self::Source) -> Option { Channels::::get(channel_id) } } impl Contains for Pallet { fn contains(channel_id: &ChannelId) -> bool { Channels::::get(channel_id).is_some() } } impl Get> for Pallet { fn get() -> PricingParametersOf { PricingParameters::::get() } } }