1#![cfg_attr(not(feature = "std"), no_std)]
19#![forbid(unsafe_code)]
20#![warn(rust_2018_idioms)]
21
22#[cfg(feature = "runtime-benchmarks")]
23mod benchmarking;
24#[cfg(test)]
25mod mock;
26#[cfg(test)]
27mod tests;
28pub mod weights;
29
30#[cfg(not(feature = "std"))]
31extern crate alloc;
32
33use domain_runtime_primitives::{MultiAccountId, TryConvertBack};
34use frame_support::dispatch::DispatchResult;
35use frame_support::ensure;
36use frame_support::traits::Currency;
37pub use pallet::*;
38use parity_scale_codec::{Decode, Encode};
39use scale_info::TypeInfo;
40use sp_domains::{DomainId, DomainsTransfersTracker, SkipBalanceChecks, Transfers};
41use sp_messenger::NoteChainTransfer;
42use sp_messenger::endpoint::EndpointResponse;
43use sp_messenger::messages::ChainId;
44use sp_runtime::traits::{CheckedAdd, CheckedSub, Get};
45use sp_std::vec;
46pub use weights::WeightInfo;
47
48#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
50pub struct Location {
51 pub chain_id: ChainId,
53 pub account_id: MultiAccountId,
55}
56
57#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
59pub struct Transfer<Balance> {
60 pub amount: Balance,
62 pub sender: Location,
64 pub receiver: Location,
66}
67
68pub(crate) type BalanceOf<T> =
70 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
71
72type MessageIdOf<T> = <<T as Config>::Sender as sp_messenger::endpoint::Sender<
73 <T as frame_system::Config>::AccountId,
74>>::MessageId;
75
76#[frame_support::pallet]
77mod pallet {
78 use crate::weights::WeightInfo;
79 use crate::{BalanceOf, Location, MessageIdOf, MultiAccountId, Transfer, TryConvertBack};
80 #[cfg(not(feature = "std"))]
81 use alloc::vec::Vec;
82 use frame_support::pallet_prelude::*;
83 use frame_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
84 use frame_support::weights::Weight;
85 use frame_system::pallet_prelude::*;
86 use parity_scale_codec::{Decode, Encode};
87 use sp_domains::{DomainId, DomainsTransfersTracker, SkipBalanceChecks, Transfers};
88 use sp_messenger::endpoint::{
89 Endpoint, EndpointHandler as EndpointHandlerT, EndpointId, EndpointRequest,
90 EndpointResponse, Sender,
91 };
92 use sp_messenger::messages::ChainId;
93 use sp_runtime::traits::Convert;
94 use sp_std::vec;
95
96 #[pallet::config]
97 pub trait Config: frame_system::Config {
98 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
100
101 type SelfChainId: Get<ChainId>;
103
104 type SelfEndpointId: Get<EndpointId>;
106
107 type Currency: Currency<Self::AccountId>;
109
110 type Sender: Sender<Self::AccountId>;
112
113 type AccountIdConverter: TryConvertBack<Self::AccountId, MultiAccountId>;
115
116 type WeightInfo: WeightInfo;
118
119 type SkipBalanceTransferChecks: SkipBalanceChecks;
121
122 type MinimumTransfer: Get<BalanceOf<Self>>;
124 }
125
126 #[pallet::pallet]
128 #[pallet::without_storage_info]
129 pub struct Pallet<T>(_);
130
131 #[pallet::storage]
133 #[pallet::getter(fn outgoing_transfers)]
134 pub(super) type OutgoingTransfers<T: Config> = StorageDoubleMap<
135 _,
136 Identity,
137 ChainId,
138 Identity,
139 MessageIdOf<T>,
140 Transfer<BalanceOf<T>>,
141 OptionQuery,
142 >;
143
144 #[pallet::storage]
146 #[pallet::getter(fn domain_balances)]
147 pub(super) type DomainBalances<T: Config> =
148 StorageMap<_, Identity, DomainId, BalanceOf<T>, ValueQuery>;
149
150 #[pallet::storage]
153 #[pallet::getter(fn chain_transfers)]
154 pub(super) type ChainTransfers<T: Config> =
155 StorageValue<_, Transfers<BalanceOf<T>>, ValueQuery>;
156
157 #[pallet::storage]
159 #[pallet::getter(fn unconfirmed_transfers)]
160 pub(super) type UnconfirmedTransfers<T: Config> =
161 StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
162
163 #[pallet::storage]
165 #[pallet::getter(fn cancelled_transfers)]
166 pub(super) type CancelledTransfers<T: Config> =
167 StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
168
169 #[pallet::event]
171 #[pallet::generate_deposit(pub (super) fn deposit_event)]
172 pub enum Event<T: Config> {
173 OutgoingTransferInitiated {
175 chain_id: ChainId,
177 message_id: MessageIdOf<T>,
179 amount: BalanceOf<T>,
181 },
182
183 OutgoingTransferFailed {
185 chain_id: ChainId,
187 message_id: MessageIdOf<T>,
189 err: DispatchError,
191 },
192
193 OutgoingTransferSuccessful {
195 chain_id: ChainId,
197 message_id: MessageIdOf<T>,
199 },
200
201 IncomingTransferSuccessful {
203 chain_id: ChainId,
205 message_id: MessageIdOf<T>,
207 amount: BalanceOf<T>,
209 },
210 }
211
212 #[pallet::error]
214 pub enum Error<T> {
215 LowBalance,
217 InvalidPayload,
219 MissingTransferRequest,
221 InvalidTransferRequest,
223 UnexpectedMessage,
225 InvalidAccountId,
227 LowBalanceOnDomain,
229 NonConsensusChain,
231 BalanceOverflow,
233 BalanceUnderflow,
235 DomainBalanceAlreadyInitialized,
237 MinimumTransferAmount,
239 }
240
241 #[pallet::call]
242 impl<T: Config> Pallet<T> {
243 #[pallet::call_index(0)]
246 #[pallet::weight(T::WeightInfo::transfer())]
247 pub fn transfer(
248 origin: OriginFor<T>,
249 dst_location: Location,
250 amount: BalanceOf<T>,
251 ) -> DispatchResult {
252 let sender = ensure_signed(origin)?;
253 ensure!(
254 amount >= T::MinimumTransfer::get(),
255 Error::<T>::MinimumTransferAmount
256 );
257
258 let _imbalance = T::Currency::withdraw(
260 &sender,
261 amount,
262 WithdrawReasons::TRANSFER,
263 ExistenceRequirement::KeepAlive,
264 )
265 .map_err(|_| Error::<T>::LowBalance)?;
266
267 let dst_chain_id = dst_location.chain_id;
269 let transfer = Transfer {
270 amount,
271 sender: Location {
272 chain_id: T::SelfChainId::get(),
273 account_id: T::AccountIdConverter::convert(sender.clone()),
274 },
275 receiver: dst_location,
276 };
277
278 let message_id = T::Sender::send_message(
280 &sender,
281 dst_chain_id,
282 EndpointRequest {
283 src_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
284 dst_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
286 payload: transfer.encode(),
287 },
288 )?;
289
290 OutgoingTransfers::<T>::insert(dst_chain_id, message_id, transfer);
291 Self::deposit_event(Event::<T>::OutgoingTransferInitiated {
292 chain_id: dst_chain_id,
293 message_id,
294 amount,
295 });
296
297 if T::SelfChainId::get().is_consensus_chain() {
300 if !T::SkipBalanceTransferChecks::should_skip_balance_check(dst_chain_id) {
301 Self::note_transfer(T::SelfChainId::get(), dst_chain_id, amount)?
302 }
303 } else {
304 ChainTransfers::<T>::try_mutate(|transfers| {
305 Self::update_transfer_out(transfers, dst_chain_id, amount)
306 })?;
307 }
308
309 Ok(())
310 }
311 }
312
313 #[pallet::hooks]
314 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
315 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
316 ChainTransfers::<T>::set(Default::default());
320 T::DbWeight::get().writes(1)
321 }
322 }
323
324 impl<T: Config> Pallet<T> {
325 pub fn transfers_storage_key() -> Vec<u8> {
326 use frame_support::storage::generator::StorageValue;
327 ChainTransfers::<T>::storage_value_final_key().to_vec()
328 }
329 }
330
331 #[derive(Debug)]
333 pub struct EndpointHandler<T>(pub PhantomData<T>);
334
335 impl<T: Config> EndpointHandlerT<MessageIdOf<T>> for EndpointHandler<T> {
336 fn message(
337 &self,
338 src_chain_id: ChainId,
339 message_id: MessageIdOf<T>,
340 req: EndpointRequest,
341 pre_check_result: DispatchResult,
342 ) -> EndpointResponse {
343 let dst_endpoint = req.dst_endpoint;
345 let req = match Transfer::decode(&mut req.payload.as_slice()) {
346 Ok(req) => req,
347 Err(_) => return Err(Error::<T>::InvalidPayload.into()),
348 };
349
350 let pre_check_handler = || {
351 ensure!(
353 T::SelfChainId::get() != src_chain_id,
354 Error::<T>::InvalidTransferRequest
355 );
356
357 ensure!(
359 dst_endpoint == Endpoint::Id(T::SelfEndpointId::get()),
360 Error::<T>::UnexpectedMessage
361 );
362
363 pre_check_result
364 };
365
366 let amount = req.amount;
367 let response = match pre_check_handler() {
368 Ok(_) => Pallet::<T>::finalize_transfer(src_chain_id, message_id, req),
369 Err(err) => Err(err),
370 };
371
372 if response.is_err() {
373 if T::SelfChainId::get().is_consensus_chain() {
376 if !T::SkipBalanceTransferChecks::should_skip_balance_check(src_chain_id) {
377 Pallet::<T>::reject_transfer(src_chain_id, T::SelfChainId::get(), amount)?;
378 }
379 } else {
380 ChainTransfers::<T>::try_mutate(|transfers| {
381 Pallet::<T>::update_transfer_rejected(transfers, src_chain_id, amount)
382 })?;
383 }
384 }
385
386 response
387 }
388
389 fn message_weight(&self) -> Weight {
390 T::WeightInfo::message()
391 }
392
393 fn message_response(
394 &self,
395 dst_chain_id: ChainId,
396 message_id: MessageIdOf<T>,
397 req: EndpointRequest,
398 resp: EndpointResponse,
399 ) -> DispatchResult {
400 let transfer = OutgoingTransfers::<T>::take(dst_chain_id, message_id)
402 .ok_or(Error::<T>::MissingTransferRequest)?;
403 ensure!(
404 req.payload == transfer.encode(),
405 Error::<T>::InvalidTransferRequest
406 );
407
408 match resp {
410 Ok(_) => {
411 frame_system::Pallet::<T>::deposit_event(
413 Into::<<T as Config>::RuntimeEvent>::into(
414 Event::<T>::OutgoingTransferSuccessful {
415 chain_id: dst_chain_id,
416 message_id,
417 },
418 ),
419 );
420 }
421 Err(err) => {
422 let account_id =
425 T::AccountIdConverter::try_convert_back(transfer.sender.account_id)
426 .ok_or(Error::<T>::InvalidAccountId)?;
427
428 if T::SelfChainId::get().is_consensus_chain() {
431 if !T::SkipBalanceTransferChecks::should_skip_balance_check(dst_chain_id) {
432 Pallet::<T>::claim_rejected_transfer(
433 T::SelfChainId::get(),
434 dst_chain_id,
435 transfer.amount,
436 )?;
437 }
438 } else {
439 ChainTransfers::<T>::try_mutate(|transfers| {
440 Pallet::<T>::update_transfer_revert(
441 transfers,
442 dst_chain_id,
443 transfer.amount,
444 )
445 })?;
446 }
447
448 let _imbalance = T::Currency::deposit_creating(&account_id, transfer.amount);
449 frame_system::Pallet::<T>::deposit_event(
450 Into::<<T as Config>::RuntimeEvent>::into(
451 Event::<T>::OutgoingTransferFailed {
452 chain_id: dst_chain_id,
453 message_id,
454 err,
455 },
456 ),
457 );
458 }
459 }
460
461 Ok(())
462 }
463
464 fn message_response_weight(&self) -> Weight {
465 T::WeightInfo::message_response()
466 }
467 }
468}
469
470impl<T: Config> sp_domains::DomainsTransfersTracker<BalanceOf<T>> for Pallet<T> {
471 type Error = Error<T>;
472
473 fn initialize_domain_balance(
474 domain_id: DomainId,
475 amount: BalanceOf<T>,
476 ) -> Result<(), Self::Error> {
477 Self::ensure_consensus_chain()?;
478
479 ensure!(
480 !DomainBalances::<T>::contains_key(domain_id),
481 Error::DomainBalanceAlreadyInitialized
482 );
483
484 DomainBalances::<T>::set(domain_id, amount);
485 Ok(())
486 }
487
488 fn note_transfer(
489 from_chain_id: ChainId,
490 to_chain_id: ChainId,
491 amount: BalanceOf<T>,
492 ) -> Result<(), Self::Error> {
493 Self::ensure_consensus_chain()?;
494
495 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
496 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
497 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
498 *current_balance = current_balance
499 .checked_sub(&amount)
500 .ok_or(Error::LowBalanceOnDomain)?;
501 Ok(())
502 })?;
503 }
504
505 *total_amount = total_amount
506 .checked_add(&amount)
507 .ok_or(Error::BalanceOverflow)?;
508 Ok(())
509 })?;
510
511 Ok(())
512 }
513
514 fn confirm_transfer(
515 from_chain_id: ChainId,
516 to_chain_id: ChainId,
517 amount: BalanceOf<T>,
518 ) -> Result<(), Self::Error> {
519 Self::ensure_consensus_chain()?;
520 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
521 *total_amount = total_amount
522 .checked_sub(&amount)
523 .ok_or(Error::BalanceUnderflow)?;
524
525 if let Some(domain_id) = to_chain_id.maybe_domain_chain() {
526 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
527 *current_balance = current_balance
528 .checked_add(&amount)
529 .ok_or(Error::BalanceOverflow)?;
530 Ok(())
531 })?;
532 }
533
534 Ok(())
535 })?;
536
537 Ok(())
538 }
539
540 fn claim_rejected_transfer(
541 from_chain_id: ChainId,
542 to_chain_id: ChainId,
543 amount: BalanceOf<T>,
544 ) -> Result<(), Self::Error> {
545 Self::ensure_consensus_chain()?;
546 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
547 *total_amount = total_amount
548 .checked_sub(&amount)
549 .ok_or(Error::BalanceUnderflow)?;
550
551 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
552 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
553 *current_balance = current_balance
554 .checked_add(&amount)
555 .ok_or(Error::BalanceOverflow)?;
556 Ok(())
557 })?;
558 }
559
560 Ok(())
561 })?;
562
563 Ok(())
564 }
565
566 fn reject_transfer(
567 from_chain_id: ChainId,
568 to_chain_id: ChainId,
569 amount: BalanceOf<T>,
570 ) -> Result<(), Self::Error> {
571 Self::ensure_consensus_chain()?;
572 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
573 *total_amount = total_amount
574 .checked_sub(&amount)
575 .ok_or(Error::BalanceUnderflow)?;
576
577 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
578 *total_amount = total_amount
579 .checked_add(&amount)
580 .ok_or(Error::BalanceOverflow)?;
581 Ok(())
582 })?;
583
584 Ok(())
585 })?;
586
587 Ok(())
588 }
589
590 fn reduce_domain_balance(domain_id: DomainId, amount: BalanceOf<T>) -> Result<(), Self::Error> {
591 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
592 *current_balance = current_balance
593 .checked_sub(&amount)
594 .ok_or(Error::LowBalanceOnDomain)?;
595 Ok(())
596 })
597 }
598}
599
600impl<T: Config> NoteChainTransfer<BalanceOf<T>> for Pallet<T> {
601 fn note_transfer_in(amount: BalanceOf<T>, from_chain_id: ChainId) -> bool {
602 if T::SelfChainId::get().is_consensus_chain() {
603 if !T::SkipBalanceTransferChecks::should_skip_balance_check(from_chain_id) {
604 Pallet::<T>::confirm_transfer(from_chain_id, T::SelfChainId::get(), amount).is_ok()
605 } else {
606 true
607 }
608 } else {
609 ChainTransfers::<T>::try_mutate(|transfers| {
610 Pallet::<T>::update_transfer_in(transfers, from_chain_id, amount)
611 })
612 .is_ok()
613 }
614 }
615
616 fn note_transfer_out(amount: BalanceOf<T>, to_chain_id: ChainId) -> bool {
617 if T::SelfChainId::get().is_consensus_chain() {
618 if !T::SkipBalanceTransferChecks::should_skip_balance_check(to_chain_id) {
619 Self::note_transfer(T::SelfChainId::get(), to_chain_id, amount).is_ok()
620 } else {
621 true
622 }
623 } else {
624 ChainTransfers::<T>::try_mutate(|transfers| {
625 Self::update_transfer_out(transfers, to_chain_id, amount)
626 })
627 .is_ok()
628 }
629 }
630}
631
632impl<T: Config> Pallet<T> {
633 fn ensure_consensus_chain() -> Result<(), Error<T>> {
634 ensure!(
635 T::SelfChainId::get().is_consensus_chain(),
636 Error::NonConsensusChain
637 );
638
639 Ok(())
640 }
641
642 fn finalize_transfer(
643 src_chain_id: ChainId,
644 message_id: MessageIdOf<T>,
645 req: Transfer<BalanceOf<T>>,
646 ) -> EndpointResponse {
647 let account_id = T::AccountIdConverter::try_convert_back(req.receiver.account_id)
649 .ok_or(Error::<T>::InvalidAccountId)?;
650
651 if T::SelfChainId::get().is_consensus_chain() {
654 if !T::SkipBalanceTransferChecks::should_skip_balance_check(src_chain_id) {
655 Pallet::<T>::confirm_transfer(src_chain_id, T::SelfChainId::get(), req.amount)?
656 }
657 } else {
658 ChainTransfers::<T>::try_mutate(|transfers| {
659 Pallet::<T>::update_transfer_in(transfers, src_chain_id, req.amount)
660 })?;
661 }
662
663 let _imbalance = T::Currency::deposit_creating(&account_id, req.amount);
664
665 frame_system::Pallet::<T>::deposit_event(Into::<<T as Config>::RuntimeEvent>::into(
666 Event::<T>::IncomingTransferSuccessful {
667 chain_id: src_chain_id,
668 message_id,
669 amount: req.amount,
670 },
671 ));
672 Ok(vec![])
673 }
674
675 fn update_transfer_out(
676 transfers: &mut Transfers<BalanceOf<T>>,
677 to_chain_id: ChainId,
678 amount: BalanceOf<T>,
679 ) -> DispatchResult {
680 let total_transfer =
681 if let Some(current_transfer_amount) = transfers.transfers_out.get(&to_chain_id) {
682 current_transfer_amount
683 .checked_add(&amount)
684 .ok_or(Error::<T>::BalanceOverflow)?
685 } else {
686 amount
687 };
688 transfers.transfers_out.insert(to_chain_id, total_transfer);
689 Ok(())
690 }
691
692 fn update_transfer_in(
693 transfers: &mut Transfers<BalanceOf<T>>,
694 from_chain_id: ChainId,
695 amount: BalanceOf<T>,
696 ) -> DispatchResult {
697 let total_transfer =
698 if let Some(current_transfer_amount) = transfers.transfers_in.get(&from_chain_id) {
699 current_transfer_amount
700 .checked_add(&amount)
701 .ok_or(Error::<T>::BalanceOverflow)?
702 } else {
703 amount
704 };
705 transfers.transfers_in.insert(from_chain_id, total_transfer);
706 Ok(())
707 }
708
709 fn update_transfer_revert(
710 transfers: &mut Transfers<BalanceOf<T>>,
711 to_chain_id: ChainId,
712 amount: BalanceOf<T>,
713 ) -> DispatchResult {
714 let total_transfer = if let Some(current_transfer_amount) =
715 transfers.rejected_transfers_claimed.get(&to_chain_id)
716 {
717 current_transfer_amount
718 .checked_add(&amount)
719 .ok_or(Error::<T>::BalanceOverflow)?
720 } else {
721 amount
722 };
723 transfers
724 .rejected_transfers_claimed
725 .insert(to_chain_id, total_transfer);
726 Ok(())
727 }
728
729 fn update_transfer_rejected(
730 transfers: &mut Transfers<BalanceOf<T>>,
731 from_chain_id: ChainId,
732 amount: BalanceOf<T>,
733 ) -> DispatchResult {
734 let total_transfer = if let Some(current_transfer_amount) =
735 transfers.transfers_rejected.get(&from_chain_id)
736 {
737 current_transfer_amount
738 .checked_add(&amount)
739 .ok_or(Error::<T>::BalanceOverflow)?
740 } else {
741 amount
742 };
743 transfers
744 .transfers_rejected
745 .insert(from_chain_id, total_transfer);
746 Ok(())
747 }
748}