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::endpoint::EndpointResponse;
42use sp_messenger::messages::ChainId;
43use sp_messenger::NoteChainTransfer;
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
122 #[pallet::pallet]
124 #[pallet::without_storage_info]
125 pub struct Pallet<T>(_);
126
127 #[pallet::storage]
129 #[pallet::getter(fn outgoing_transfers)]
130 pub(super) type OutgoingTransfers<T: Config> = StorageDoubleMap<
131 _,
132 Identity,
133 ChainId,
134 Identity,
135 MessageIdOf<T>,
136 Transfer<BalanceOf<T>>,
137 OptionQuery,
138 >;
139
140 #[pallet::storage]
142 #[pallet::getter(fn domain_balances)]
143 pub(super) type DomainBalances<T: Config> =
144 StorageMap<_, Identity, DomainId, BalanceOf<T>, ValueQuery>;
145
146 #[pallet::storage]
149 #[pallet::getter(fn chain_transfers)]
150 pub(super) type ChainTransfers<T: Config> =
151 StorageValue<_, Transfers<BalanceOf<T>>, ValueQuery>;
152
153 #[pallet::storage]
155 #[pallet::getter(fn unconfirmed_transfers)]
156 pub(super) type UnconfirmedTransfers<T: Config> =
157 StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
158
159 #[pallet::storage]
161 #[pallet::getter(fn cancelled_transfers)]
162 pub(super) type CancelledTransfers<T: Config> =
163 StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
164
165 #[pallet::event]
167 #[pallet::generate_deposit(pub (super) fn deposit_event)]
168 pub enum Event<T: Config> {
169 OutgoingTransferInitiated {
171 chain_id: ChainId,
173 message_id: MessageIdOf<T>,
175 amount: BalanceOf<T>,
177 },
178
179 OutgoingTransferFailed {
181 chain_id: ChainId,
183 message_id: MessageIdOf<T>,
185 err: DispatchError,
187 },
188
189 OutgoingTransferSuccessful {
191 chain_id: ChainId,
193 message_id: MessageIdOf<T>,
195 },
196
197 IncomingTransferSuccessful {
199 chain_id: ChainId,
201 message_id: MessageIdOf<T>,
203 amount: BalanceOf<T>,
205 },
206 }
207
208 #[pallet::error]
210 pub enum Error<T> {
211 LowBalance,
213 InvalidPayload,
215 MissingTransferRequest,
217 InvalidTransferRequest,
219 UnexpectedMessage,
221 InvalidAccountId,
223 LowBalanceOnDomain,
225 NonConsensusChain,
227 BalanceOverflow,
229 BalanceUnderflow,
231 DomainBalanceAlreadyInitialized,
233 }
234
235 #[pallet::call]
236 impl<T: Config> Pallet<T> {
237 #[pallet::call_index(0)]
240 #[pallet::weight(T::WeightInfo::transfer())]
241 pub fn transfer(
242 origin: OriginFor<T>,
243 dst_location: Location,
244 amount: BalanceOf<T>,
245 ) -> DispatchResult {
246 let sender = ensure_signed(origin)?;
247
248 let _imbalance = T::Currency::withdraw(
250 &sender,
251 amount,
252 WithdrawReasons::TRANSFER,
253 ExistenceRequirement::KeepAlive,
254 )
255 .map_err(|_| Error::<T>::LowBalance)?;
256
257 let dst_chain_id = dst_location.chain_id;
259 let transfer = Transfer {
260 amount,
261 sender: Location {
262 chain_id: T::SelfChainId::get(),
263 account_id: T::AccountIdConverter::convert(sender.clone()),
264 },
265 receiver: dst_location,
266 };
267
268 let message_id = T::Sender::send_message(
270 &sender,
271 dst_chain_id,
272 EndpointRequest {
273 src_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
274 dst_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
276 payload: transfer.encode(),
277 },
278 )?;
279
280 OutgoingTransfers::<T>::insert(dst_chain_id, message_id, transfer);
281 Self::deposit_event(Event::<T>::OutgoingTransferInitiated {
282 chain_id: dst_chain_id,
283 message_id,
284 amount,
285 });
286
287 if T::SelfChainId::get().is_consensus_chain() {
290 if !T::SkipBalanceTransferChecks::should_skip_balance_check(dst_chain_id) {
291 Self::note_transfer(T::SelfChainId::get(), dst_chain_id, amount)?
292 }
293 } else {
294 ChainTransfers::<T>::try_mutate(|transfers| {
295 Self::update_transfer_out(transfers, dst_chain_id, amount)
296 })?;
297 }
298
299 Ok(())
300 }
301 }
302
303 #[pallet::hooks]
304 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
305 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
306 ChainTransfers::<T>::set(Default::default());
310 T::DbWeight::get().writes(1)
311 }
312 }
313
314 impl<T: Config> Pallet<T> {
315 pub fn transfers_storage_key() -> Vec<u8> {
316 use frame_support::storage::generator::StorageValue;
317 ChainTransfers::<T>::storage_value_final_key().to_vec()
318 }
319 }
320
321 #[derive(Debug)]
323 pub struct EndpointHandler<T>(pub PhantomData<T>);
324
325 impl<T: Config> EndpointHandlerT<MessageIdOf<T>> for EndpointHandler<T> {
326 fn message(
327 &self,
328 src_chain_id: ChainId,
329 message_id: MessageIdOf<T>,
330 req: EndpointRequest,
331 pre_check_result: DispatchResult,
332 ) -> EndpointResponse {
333 let dst_endpoint = req.dst_endpoint;
335 let req = match Transfer::decode(&mut req.payload.as_slice()) {
336 Ok(req) => req,
337 Err(_) => return Err(Error::<T>::InvalidPayload.into()),
338 };
339
340 let pre_check_handler = || {
341 ensure!(
343 T::SelfChainId::get() != src_chain_id,
344 Error::<T>::InvalidTransferRequest
345 );
346
347 ensure!(
349 dst_endpoint == Endpoint::Id(T::SelfEndpointId::get()),
350 Error::<T>::UnexpectedMessage
351 );
352
353 pre_check_result
354 };
355
356 let amount = req.amount;
357 let response = match pre_check_handler() {
358 Ok(_) => Pallet::<T>::finalize_transfer(src_chain_id, message_id, req),
359 Err(err) => Err(err),
360 };
361
362 if response.is_err() {
363 if T::SelfChainId::get().is_consensus_chain() {
366 if !T::SkipBalanceTransferChecks::should_skip_balance_check(src_chain_id) {
367 Pallet::<T>::reject_transfer(src_chain_id, T::SelfChainId::get(), amount)?;
368 }
369 } else {
370 ChainTransfers::<T>::try_mutate(|transfers| {
371 Pallet::<T>::update_transfer_rejected(transfers, src_chain_id, amount)
372 })?;
373 }
374 }
375
376 response
377 }
378
379 fn message_weight(&self) -> Weight {
380 T::WeightInfo::message()
381 }
382
383 fn message_response(
384 &self,
385 dst_chain_id: ChainId,
386 message_id: MessageIdOf<T>,
387 req: EndpointRequest,
388 resp: EndpointResponse,
389 ) -> DispatchResult {
390 let transfer = OutgoingTransfers::<T>::take(dst_chain_id, message_id)
392 .ok_or(Error::<T>::MissingTransferRequest)?;
393 ensure!(
394 req.payload == transfer.encode(),
395 Error::<T>::InvalidTransferRequest
396 );
397
398 match resp {
400 Ok(_) => {
401 frame_system::Pallet::<T>::deposit_event(
403 Into::<<T as Config>::RuntimeEvent>::into(
404 Event::<T>::OutgoingTransferSuccessful {
405 chain_id: dst_chain_id,
406 message_id,
407 },
408 ),
409 );
410 }
411 Err(err) => {
412 let account_id =
415 T::AccountIdConverter::try_convert_back(transfer.sender.account_id)
416 .ok_or(Error::<T>::InvalidAccountId)?;
417
418 if T::SelfChainId::get().is_consensus_chain() {
421 if !T::SkipBalanceTransferChecks::should_skip_balance_check(dst_chain_id) {
422 Pallet::<T>::claim_rejected_transfer(
423 T::SelfChainId::get(),
424 dst_chain_id,
425 transfer.amount,
426 )?;
427 }
428 } else {
429 ChainTransfers::<T>::try_mutate(|transfers| {
430 Pallet::<T>::update_transfer_revert(
431 transfers,
432 dst_chain_id,
433 transfer.amount,
434 )
435 })?;
436 }
437
438 let _imbalance = T::Currency::deposit_creating(&account_id, transfer.amount);
439 frame_system::Pallet::<T>::deposit_event(
440 Into::<<T as Config>::RuntimeEvent>::into(
441 Event::<T>::OutgoingTransferFailed {
442 chain_id: dst_chain_id,
443 message_id,
444 err,
445 },
446 ),
447 );
448 }
449 }
450
451 Ok(())
452 }
453
454 fn message_response_weight(&self) -> Weight {
455 T::WeightInfo::message_response()
456 }
457 }
458}
459
460impl<T: Config> sp_domains::DomainsTransfersTracker<BalanceOf<T>> for Pallet<T> {
461 type Error = Error<T>;
462
463 fn initialize_domain_balance(
464 domain_id: DomainId,
465 amount: BalanceOf<T>,
466 ) -> Result<(), Self::Error> {
467 Self::ensure_consensus_chain()?;
468
469 ensure!(
470 !DomainBalances::<T>::contains_key(domain_id),
471 Error::DomainBalanceAlreadyInitialized
472 );
473
474 DomainBalances::<T>::set(domain_id, amount);
475 Ok(())
476 }
477
478 fn note_transfer(
479 from_chain_id: ChainId,
480 to_chain_id: ChainId,
481 amount: BalanceOf<T>,
482 ) -> Result<(), Self::Error> {
483 Self::ensure_consensus_chain()?;
484
485 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
486 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
487 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
488 *current_balance = current_balance
489 .checked_sub(&amount)
490 .ok_or(Error::LowBalanceOnDomain)?;
491 Ok(())
492 })?;
493 }
494
495 *total_amount = total_amount
496 .checked_add(&amount)
497 .ok_or(Error::BalanceOverflow)?;
498 Ok(())
499 })?;
500
501 Ok(())
502 }
503
504 fn confirm_transfer(
505 from_chain_id: ChainId,
506 to_chain_id: ChainId,
507 amount: BalanceOf<T>,
508 ) -> Result<(), Self::Error> {
509 Self::ensure_consensus_chain()?;
510 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
511 *total_amount = total_amount
512 .checked_sub(&amount)
513 .ok_or(Error::BalanceUnderflow)?;
514
515 if let Some(domain_id) = to_chain_id.maybe_domain_chain() {
516 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
517 *current_balance = current_balance
518 .checked_add(&amount)
519 .ok_or(Error::BalanceOverflow)?;
520 Ok(())
521 })?;
522 }
523
524 Ok(())
525 })?;
526
527 Ok(())
528 }
529
530 fn claim_rejected_transfer(
531 from_chain_id: ChainId,
532 to_chain_id: ChainId,
533 amount: BalanceOf<T>,
534 ) -> Result<(), Self::Error> {
535 Self::ensure_consensus_chain()?;
536 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
537 *total_amount = total_amount
538 .checked_sub(&amount)
539 .ok_or(Error::BalanceUnderflow)?;
540
541 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
542 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
543 *current_balance = current_balance
544 .checked_add(&amount)
545 .ok_or(Error::BalanceOverflow)?;
546 Ok(())
547 })?;
548 }
549
550 Ok(())
551 })?;
552
553 Ok(())
554 }
555
556 fn reject_transfer(
557 from_chain_id: ChainId,
558 to_chain_id: ChainId,
559 amount: BalanceOf<T>,
560 ) -> Result<(), Self::Error> {
561 Self::ensure_consensus_chain()?;
562 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
563 *total_amount = total_amount
564 .checked_sub(&amount)
565 .ok_or(Error::BalanceUnderflow)?;
566
567 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
568 *total_amount = total_amount
569 .checked_add(&amount)
570 .ok_or(Error::BalanceOverflow)?;
571 Ok(())
572 })?;
573
574 Ok(())
575 })?;
576
577 Ok(())
578 }
579
580 fn reduce_domain_balance(domain_id: DomainId, amount: BalanceOf<T>) -> Result<(), Self::Error> {
581 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
582 *current_balance = current_balance
583 .checked_sub(&amount)
584 .ok_or(Error::LowBalanceOnDomain)?;
585 Ok(())
586 })
587 }
588}
589
590impl<T: Config> NoteChainTransfer<BalanceOf<T>> for Pallet<T> {
591 fn note_transfer_in(amount: BalanceOf<T>, from_chain_id: ChainId) -> bool {
592 if T::SelfChainId::get().is_consensus_chain() {
593 if !T::SkipBalanceTransferChecks::should_skip_balance_check(from_chain_id) {
594 Pallet::<T>::confirm_transfer(from_chain_id, T::SelfChainId::get(), amount).is_ok()
595 } else {
596 true
597 }
598 } else {
599 ChainTransfers::<T>::try_mutate(|transfers| {
600 Pallet::<T>::update_transfer_in(transfers, from_chain_id, amount)
601 })
602 .is_ok()
603 }
604 }
605
606 fn note_transfer_out(amount: BalanceOf<T>, to_chain_id: ChainId) -> bool {
607 if T::SelfChainId::get().is_consensus_chain() {
608 if !T::SkipBalanceTransferChecks::should_skip_balance_check(to_chain_id) {
609 Self::note_transfer(T::SelfChainId::get(), to_chain_id, amount).is_ok()
610 } else {
611 true
612 }
613 } else {
614 ChainTransfers::<T>::try_mutate(|transfers| {
615 Self::update_transfer_out(transfers, to_chain_id, amount)
616 })
617 .is_ok()
618 }
619 }
620}
621
622impl<T: Config> Pallet<T> {
623 fn ensure_consensus_chain() -> Result<(), Error<T>> {
624 ensure!(
625 T::SelfChainId::get().is_consensus_chain(),
626 Error::NonConsensusChain
627 );
628
629 Ok(())
630 }
631
632 fn finalize_transfer(
633 src_chain_id: ChainId,
634 message_id: MessageIdOf<T>,
635 req: Transfer<BalanceOf<T>>,
636 ) -> EndpointResponse {
637 let account_id = T::AccountIdConverter::try_convert_back(req.receiver.account_id)
639 .ok_or(Error::<T>::InvalidAccountId)?;
640
641 if T::SelfChainId::get().is_consensus_chain() {
644 if !T::SkipBalanceTransferChecks::should_skip_balance_check(src_chain_id) {
645 Pallet::<T>::confirm_transfer(src_chain_id, T::SelfChainId::get(), req.amount)?
646 }
647 } else {
648 ChainTransfers::<T>::try_mutate(|transfers| {
649 Pallet::<T>::update_transfer_in(transfers, src_chain_id, req.amount)
650 })?;
651 }
652
653 let _imbalance = T::Currency::deposit_creating(&account_id, req.amount);
654
655 frame_system::Pallet::<T>::deposit_event(Into::<<T as Config>::RuntimeEvent>::into(
656 Event::<T>::IncomingTransferSuccessful {
657 chain_id: src_chain_id,
658 message_id,
659 amount: req.amount,
660 },
661 ));
662 Ok(vec![])
663 }
664
665 fn update_transfer_out(
666 transfers: &mut Transfers<BalanceOf<T>>,
667 to_chain_id: ChainId,
668 amount: BalanceOf<T>,
669 ) -> DispatchResult {
670 let total_transfer =
671 if let Some(current_transfer_amount) = transfers.transfers_out.get(&to_chain_id) {
672 current_transfer_amount
673 .checked_add(&amount)
674 .ok_or(Error::<T>::BalanceOverflow)?
675 } else {
676 amount
677 };
678 transfers.transfers_out.insert(to_chain_id, total_transfer);
679 Ok(())
680 }
681
682 fn update_transfer_in(
683 transfers: &mut Transfers<BalanceOf<T>>,
684 from_chain_id: ChainId,
685 amount: BalanceOf<T>,
686 ) -> DispatchResult {
687 let total_transfer =
688 if let Some(current_transfer_amount) = transfers.transfers_in.get(&from_chain_id) {
689 current_transfer_amount
690 .checked_add(&amount)
691 .ok_or(Error::<T>::BalanceOverflow)?
692 } else {
693 amount
694 };
695 transfers.transfers_in.insert(from_chain_id, total_transfer);
696 Ok(())
697 }
698
699 fn update_transfer_revert(
700 transfers: &mut Transfers<BalanceOf<T>>,
701 to_chain_id: ChainId,
702 amount: BalanceOf<T>,
703 ) -> DispatchResult {
704 let total_transfer = if let Some(current_transfer_amount) =
705 transfers.rejected_transfers_claimed.get(&to_chain_id)
706 {
707 current_transfer_amount
708 .checked_add(&amount)
709 .ok_or(Error::<T>::BalanceOverflow)?
710 } else {
711 amount
712 };
713 transfers
714 .rejected_transfers_claimed
715 .insert(to_chain_id, total_transfer);
716 Ok(())
717 }
718
719 fn update_transfer_rejected(
720 transfers: &mut Transfers<BalanceOf<T>>,
721 from_chain_id: ChainId,
722 amount: BalanceOf<T>,
723 ) -> DispatchResult {
724 let total_transfer = if let Some(current_transfer_amount) =
725 transfers.transfers_rejected.get(&from_chain_id)
726 {
727 current_transfer_amount
728 .checked_add(&amount)
729 .ok_or(Error::<T>::BalanceOverflow)?
730 } else {
731 amount
732 };
733 transfers
734 .transfers_rejected
735 .insert(from_chain_id, total_transfer);
736 Ok(())
737 }
738}