#![cfg_attr(not(feature = "std"), no_std)]
#![forbid(unsafe_code)]
#![warn(rust_2018_idioms)]
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
#[cfg(test)]
mod mock;
#[cfg(test)]
mod tests;
pub mod weights;
#[cfg(not(feature = "std"))]
extern crate alloc;
use codec::{Decode, Encode};
use domain_runtime_primitives::{MultiAccountId, TryConvertBack};
use frame_support::dispatch::DispatchResult;
use frame_support::ensure;
use frame_support::traits::Currency;
pub use pallet::*;
use scale_info::TypeInfo;
use sp_domains::{DomainId, DomainsTransfersTracker, Transfers};
use sp_messenger::endpoint::EndpointResponse;
use sp_messenger::messages::ChainId;
use sp_runtime::traits::{CheckedAdd, CheckedSub, Get};
use sp_std::vec;
#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
pub struct Location {
pub chain_id: ChainId,
pub account_id: MultiAccountId,
}
#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
pub struct Transfer<Balance> {
pub amount: Balance,
pub sender: Location,
pub receiver: Location,
}
pub(crate) type BalanceOf<T> =
<<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
type MessageIdOf<T> = <<T as Config>::Sender as sp_messenger::endpoint::Sender<
<T as frame_system::Config>::AccountId,
>>::MessageId;
#[frame_support::pallet]
mod pallet {
use crate::weights::WeightInfo;
use crate::{BalanceOf, Location, MessageIdOf, MultiAccountId, Transfer, TryConvertBack};
#[cfg(not(feature = "std"))]
use alloc::vec::Vec;
use codec::{Decode, Encode};
use frame_support::pallet_prelude::*;
use frame_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
use frame_support::weights::Weight;
use frame_system::pallet_prelude::*;
use sp_domains::{DomainId, DomainsTransfersTracker, Transfers};
use sp_messenger::endpoint::{
Endpoint, EndpointHandler as EndpointHandlerT, EndpointId, EndpointRequest,
EndpointResponse, Sender,
};
use sp_messenger::messages::ChainId;
use sp_runtime::traits::Convert;
use sp_std::vec;
#[pallet::config]
pub trait Config: frame_system::Config {
type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
type SelfChainId: Get<ChainId>;
type SelfEndpointId: Get<EndpointId>;
type Currency: Currency<Self::AccountId>;
type Sender: Sender<Self::AccountId>;
type AccountIdConverter: TryConvertBack<Self::AccountId, MultiAccountId>;
type WeightInfo: WeightInfo;
}
#[pallet::pallet]
#[pallet::without_storage_info]
pub struct Pallet<T>(_);
#[pallet::storage]
#[pallet::getter(fn outgoing_transfers)]
pub(super) type OutgoingTransfers<T: Config> = StorageDoubleMap<
_,
Identity,
ChainId,
Identity,
MessageIdOf<T>,
Transfer<BalanceOf<T>>,
OptionQuery,
>;
#[pallet::storage]
#[pallet::getter(fn domain_balances)]
pub(super) type DomainBalances<T: Config> =
StorageMap<_, Identity, DomainId, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn chain_transfers)]
pub(super) type ChainTransfers<T: Config> =
StorageValue<_, Transfers<BalanceOf<T>>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn unconfirmed_transfers)]
pub(super) type UnconfirmedTransfers<T: Config> =
StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
#[pallet::storage]
#[pallet::getter(fn cancelled_transfers)]
pub(super) type CancelledTransfers<T: Config> =
StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
#[pallet::event]
#[pallet::generate_deposit(pub (super) fn deposit_event)]
pub enum Event<T: Config> {
OutgoingTransferInitiated {
chain_id: ChainId,
message_id: MessageIdOf<T>,
},
OutgoingTransferFailed {
chain_id: ChainId,
message_id: MessageIdOf<T>,
err: DispatchError,
},
OutgoingTransferSuccessful {
chain_id: ChainId,
message_id: MessageIdOf<T>,
},
IncomingTransferSuccessful {
chain_id: ChainId,
message_id: MessageIdOf<T>,
},
}
#[pallet::error]
pub enum Error<T> {
LowBalance,
InvalidPayload,
MissingTransferRequest,
InvalidTransferRequest,
UnexpectedMessage,
InvalidAccountId,
LowBalanceOnDomain,
NonConsensusChain,
BalanceOverflow,
BalanceUnderflow,
DomainBalanceAlreadyInitialized,
}
#[pallet::call]
impl<T: Config> Pallet<T> {
#[pallet::call_index(0)]
#[pallet::weight(T::WeightInfo::transfer())]
pub fn transfer(
origin: OriginFor<T>,
dst_location: Location,
amount: BalanceOf<T>,
) -> DispatchResult {
let sender = ensure_signed(origin)?;
let _imbalance = T::Currency::withdraw(
&sender,
amount,
WithdrawReasons::TRANSFER,
ExistenceRequirement::KeepAlive,
)
.map_err(|_| Error::<T>::LowBalance)?;
let dst_chain_id = dst_location.chain_id;
let transfer = Transfer {
amount,
sender: Location {
chain_id: T::SelfChainId::get(),
account_id: T::AccountIdConverter::convert(sender.clone()),
},
receiver: dst_location,
};
let message_id = T::Sender::send_message(
&sender,
dst_chain_id,
EndpointRequest {
src_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
dst_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
payload: transfer.encode(),
},
)?;
OutgoingTransfers::<T>::insert(dst_chain_id, message_id, transfer);
Self::deposit_event(Event::<T>::OutgoingTransferInitiated {
chain_id: dst_chain_id,
message_id,
});
if T::SelfChainId::get().is_consensus_chain() {
Self::note_transfer(T::SelfChainId::get(), dst_chain_id, amount)?
} else {
ChainTransfers::<T>::try_mutate(|transfers| {
Self::update_transfer_out(transfers, dst_chain_id, amount)
})?;
}
Ok(())
}
}
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
ChainTransfers::<T>::set(Default::default());
T::DbWeight::get().writes(1)
}
}
impl<T: Config> Pallet<T> {
pub fn transfers_storage_key() -> Vec<u8> {
use frame_support::storage::generator::StorageValue;
ChainTransfers::<T>::storage_value_final_key().to_vec()
}
}
#[derive(Debug)]
pub struct EndpointHandler<T>(pub PhantomData<T>);
impl<T: Config> EndpointHandlerT<MessageIdOf<T>> for EndpointHandler<T> {
fn message(
&self,
src_chain_id: ChainId,
message_id: MessageIdOf<T>,
req: EndpointRequest,
) -> EndpointResponse {
ensure!(
T::SelfChainId::get() != src_chain_id,
Error::<T>::InvalidTransferRequest
);
ensure!(
req.dst_endpoint == Endpoint::Id(T::SelfEndpointId::get()),
Error::<T>::UnexpectedMessage
);
let req = match Transfer::decode(&mut req.payload.as_slice()) {
Ok(req) => req,
Err(_) => return Err(Error::<T>::InvalidPayload.into()),
};
let amount = req.amount;
let response = Pallet::<T>::finalize_transfer(src_chain_id, message_id, req);
if response.is_err() {
if T::SelfChainId::get().is_consensus_chain() {
Pallet::<T>::reject_transfer(src_chain_id, T::SelfChainId::get(), amount)?;
} else {
ChainTransfers::<T>::try_mutate(|transfers| {
Pallet::<T>::update_transfer_rejected(transfers, src_chain_id, amount)
})?;
}
}
response
}
fn message_weight(&self) -> Weight {
T::WeightInfo::message()
}
fn message_response(
&self,
dst_chain_id: ChainId,
message_id: MessageIdOf<T>,
req: EndpointRequest,
resp: EndpointResponse,
) -> DispatchResult {
let transfer = OutgoingTransfers::<T>::take(dst_chain_id, message_id)
.ok_or(Error::<T>::MissingTransferRequest)?;
ensure!(
req.payload == transfer.encode(),
Error::<T>::InvalidTransferRequest
);
match resp {
Ok(_) => {
frame_system::Pallet::<T>::deposit_event(
Into::<<T as Config>::RuntimeEvent>::into(
Event::<T>::OutgoingTransferSuccessful {
chain_id: dst_chain_id,
message_id,
},
),
);
}
Err(err) => {
let account_id =
T::AccountIdConverter::try_convert_back(transfer.sender.account_id)
.ok_or(Error::<T>::InvalidAccountId)?;
let _imbalance = T::Currency::deposit_creating(&account_id, transfer.amount);
if T::SelfChainId::get().is_consensus_chain() {
Pallet::<T>::claim_rejected_transfer(
T::SelfChainId::get(),
dst_chain_id,
transfer.amount,
)?;
} else {
ChainTransfers::<T>::try_mutate(|transfers| {
Pallet::<T>::update_transfer_revert(
transfers,
dst_chain_id,
transfer.amount,
)
})?;
}
frame_system::Pallet::<T>::deposit_event(
Into::<<T as Config>::RuntimeEvent>::into(
Event::<T>::OutgoingTransferFailed {
chain_id: dst_chain_id,
message_id,
err,
},
),
);
}
}
Ok(())
}
fn message_response_weight(&self) -> Weight {
T::WeightInfo::message_response()
}
}
}
impl<T: Config> sp_domains::DomainsTransfersTracker<BalanceOf<T>> for Pallet<T> {
type Error = Error<T>;
fn initialize_domain_balance(
domain_id: DomainId,
amount: BalanceOf<T>,
) -> Result<(), Self::Error> {
Self::ensure_consensus_chain()?;
ensure!(
!DomainBalances::<T>::contains_key(domain_id),
Error::DomainBalanceAlreadyInitialized
);
DomainBalances::<T>::set(domain_id, amount);
Ok(())
}
fn note_transfer(
from_chain_id: ChainId,
to_chain_id: ChainId,
amount: BalanceOf<T>,
) -> Result<(), Self::Error> {
Self::ensure_consensus_chain()?;
if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
*current_balance = current_balance
.checked_sub(&amount)
.ok_or(Error::LowBalanceOnDomain)?;
Ok(())
})?;
}
UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
*total_amount = total_amount
.checked_add(&amount)
.ok_or(Error::BalanceOverflow)?;
Ok(())
})?;
Ok(())
}
fn confirm_transfer(
from_chain_id: ChainId,
to_chain_id: ChainId,
amount: BalanceOf<T>,
) -> Result<(), Self::Error> {
Self::ensure_consensus_chain()?;
UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
*total_amount = total_amount
.checked_sub(&amount)
.ok_or(Error::BalanceUnderflow)?;
Ok(())
})?;
if let Some(domain_id) = to_chain_id.maybe_domain_chain() {
DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
*current_balance = current_balance
.checked_add(&amount)
.ok_or(Error::BalanceOverflow)?;
Ok(())
})?;
}
Ok(())
}
fn claim_rejected_transfer(
from_chain_id: ChainId,
to_chain_id: ChainId,
amount: BalanceOf<T>,
) -> Result<(), Self::Error> {
Self::ensure_consensus_chain()?;
CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
*total_amount = total_amount
.checked_sub(&amount)
.ok_or(Error::BalanceUnderflow)?;
Ok(())
})?;
if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
*current_balance = current_balance
.checked_add(&amount)
.ok_or(Error::BalanceOverflow)?;
Ok(())
})?;
}
Ok(())
}
fn reject_transfer(
from_chain_id: ChainId,
to_chain_id: ChainId,
amount: BalanceOf<T>,
) -> Result<(), Self::Error> {
Self::ensure_consensus_chain()?;
UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
*total_amount = total_amount
.checked_sub(&amount)
.ok_or(Error::BalanceUnderflow)?;
Ok(())
})?;
CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
*total_amount = total_amount
.checked_add(&amount)
.ok_or(Error::BalanceOverflow)?;
Ok(())
})?;
Ok(())
}
fn reduce_domain_balance(domain_id: DomainId, amount: BalanceOf<T>) -> Result<(), Self::Error> {
DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
*current_balance = current_balance
.checked_sub(&amount)
.ok_or(Error::LowBalanceOnDomain)?;
Ok(())
})
}
}
impl<T: Config> Pallet<T> {
fn ensure_consensus_chain() -> Result<(), Error<T>> {
ensure!(
T::SelfChainId::get().is_consensus_chain(),
Error::NonConsensusChain
);
Ok(())
}
fn finalize_transfer(
src_chain_id: ChainId,
message_id: MessageIdOf<T>,
req: Transfer<BalanceOf<T>>,
) -> EndpointResponse {
let account_id = T::AccountIdConverter::try_convert_back(req.receiver.account_id)
.ok_or(Error::<T>::InvalidAccountId)?;
let _imbalance = T::Currency::deposit_creating(&account_id, req.amount);
if T::SelfChainId::get().is_consensus_chain() {
Pallet::<T>::confirm_transfer(src_chain_id, T::SelfChainId::get(), req.amount)?
} else {
ChainTransfers::<T>::try_mutate(|transfers| {
Pallet::<T>::update_transfer_in(transfers, src_chain_id, req.amount)
})?;
}
frame_system::Pallet::<T>::deposit_event(Into::<<T as Config>::RuntimeEvent>::into(
Event::<T>::IncomingTransferSuccessful {
chain_id: src_chain_id,
message_id,
},
));
Ok(vec![])
}
fn update_transfer_out(
transfers: &mut Transfers<BalanceOf<T>>,
to_chain_id: ChainId,
amount: BalanceOf<T>,
) -> DispatchResult {
let total_transfer =
if let Some(current_transfer_amount) = transfers.transfers_out.get(&to_chain_id) {
current_transfer_amount
.checked_add(&amount)
.ok_or(Error::<T>::BalanceOverflow)?
} else {
amount
};
transfers.transfers_out.insert(to_chain_id, total_transfer);
Ok(())
}
fn update_transfer_in(
transfers: &mut Transfers<BalanceOf<T>>,
from_chain_id: ChainId,
amount: BalanceOf<T>,
) -> DispatchResult {
let total_transfer =
if let Some(current_transfer_amount) = transfers.transfers_in.get(&from_chain_id) {
current_transfer_amount
.checked_add(&amount)
.ok_or(Error::<T>::BalanceOverflow)?
} else {
amount
};
transfers.transfers_in.insert(from_chain_id, total_transfer);
Ok(())
}
fn update_transfer_revert(
transfers: &mut Transfers<BalanceOf<T>>,
to_chain_id: ChainId,
amount: BalanceOf<T>,
) -> DispatchResult {
let total_transfer = if let Some(current_transfer_amount) =
transfers.rejected_transfers_claimed.get(&to_chain_id)
{
current_transfer_amount
.checked_add(&amount)
.ok_or(Error::<T>::BalanceOverflow)?
} else {
amount
};
transfers
.rejected_transfers_claimed
.insert(to_chain_id, total_transfer);
Ok(())
}
fn update_transfer_rejected(
transfers: &mut Transfers<BalanceOf<T>>,
from_chain_id: ChainId,
amount: BalanceOf<T>,
) -> DispatchResult {
let total_transfer = if let Some(current_transfer_amount) =
transfers.transfers_rejected.get(&from_chain_id)
{
current_transfer_amount
.checked_add(&amount)
.ok_or(Error::<T>::BalanceOverflow)?
} else {
amount
};
transfers
.transfers_rejected
.insert(from_chain_id, total_transfer);
Ok(())
}
}