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