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::execution_receipt::Transfers;
41use sp_domains::{DomainId, DomainsTransfersTracker};
42use sp_messenger::NoteChainTransfer;
43use sp_messenger::endpoint::EndpointResponse;
44use sp_messenger::messages::ChainId;
45use sp_runtime::traits::{CheckedAdd, CheckedSub, Get};
46use sp_std::vec;
47pub use weights::WeightInfo;
48
49#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
51pub struct Location {
52 pub chain_id: ChainId,
54 pub account_id: MultiAccountId,
56}
57
58#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
60pub struct Transfer<Balance> {
61 pub amount: Balance,
63 pub sender: Location,
65 pub receiver: Location,
67}
68
69pub(crate) type BalanceOf<T> =
71 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
72
73type MessageIdOf<T> = <<T as Config>::Sender as sp_messenger::endpoint::Sender<
74 <T as frame_system::Config>::AccountId,
75>>::MessageId;
76
77#[frame_support::pallet]
78mod pallet {
79 use crate::weights::WeightInfo;
80 use crate::{BalanceOf, Location, MessageIdOf, MultiAccountId, Transfer, TryConvertBack};
81 #[cfg(not(feature = "std"))]
82 use alloc::vec::Vec;
83 use frame_support::pallet_prelude::*;
84 use frame_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
85 use frame_support::weights::Weight;
86 use frame_system::pallet_prelude::*;
87 use parity_scale_codec::{Decode, Encode};
88 use sp_domains::execution_receipt::Transfers;
89 use sp_domains::{DomainId, DomainsTransfersTracker};
90 use sp_messenger::endpoint::{
91 Endpoint, EndpointHandler as EndpointHandlerT, EndpointId, EndpointRequest,
92 EndpointResponse, Sender,
93 };
94 use sp_messenger::messages::ChainId;
95 use sp_runtime::traits::Convert;
96 use sp_std::vec;
97
98 #[pallet::config]
99 pub trait Config: frame_system::Config {
100 type RuntimeEvent: From<Event<Self>> + IsType<<Self as frame_system::Config>::RuntimeEvent>;
102
103 type SelfChainId: Get<ChainId>;
105
106 type SelfEndpointId: Get<EndpointId>;
108
109 type Currency: Currency<Self::AccountId>;
111
112 type Sender: Sender<Self::AccountId>;
114
115 type AccountIdConverter: TryConvertBack<Self::AccountId, MultiAccountId>;
117
118 type WeightInfo: WeightInfo;
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 Self::note_transfer(T::SelfChainId::get(), dst_chain_id, amount)?
300 } else {
301 ChainTransfers::<T>::try_mutate(|transfers| {
302 Self::update_transfer_out(transfers, dst_chain_id, amount)
303 })?;
304 }
305
306 Ok(())
307 }
308 }
309
310 #[pallet::hooks]
311 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
312 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
313 ChainTransfers::<T>::set(Default::default());
317 T::DbWeight::get().writes(1)
318 }
319 }
320
321 impl<T: Config> Pallet<T> {
322 pub fn transfers_storage_key() -> Vec<u8> {
323 use frame_support::storage::generator::StorageValue;
324 ChainTransfers::<T>::storage_value_final_key().to_vec()
325 }
326 }
327
328 #[derive(Debug)]
330 pub struct EndpointHandler<T>(pub PhantomData<T>);
331
332 impl<T: Config> EndpointHandlerT<MessageIdOf<T>> for EndpointHandler<T> {
333 fn message(
334 &self,
335 src_chain_id: ChainId,
336 message_id: MessageIdOf<T>,
337 req: EndpointRequest,
338 pre_check_result: DispatchResult,
339 ) -> EndpointResponse {
340 let dst_endpoint = req.dst_endpoint;
342 let req = match Transfer::decode(&mut req.payload.as_slice()) {
343 Ok(req) => req,
344 Err(_) => return Err(Error::<T>::InvalidPayload.into()),
345 };
346
347 let pre_check_handler = || {
348 ensure!(
350 T::SelfChainId::get() != src_chain_id,
351 Error::<T>::InvalidTransferRequest
352 );
353
354 ensure!(
356 dst_endpoint == Endpoint::Id(T::SelfEndpointId::get()),
357 Error::<T>::UnexpectedMessage
358 );
359
360 pre_check_result
361 };
362
363 let amount = req.amount;
364 let response = match pre_check_handler() {
365 Ok(_) => Pallet::<T>::finalize_transfer(src_chain_id, message_id, req),
366 Err(err) => Err(err),
367 };
368
369 if response.is_err() {
370 if T::SelfChainId::get().is_consensus_chain() {
373 Pallet::<T>::reject_transfer(src_chain_id, T::SelfChainId::get(), amount)?;
374 } else {
375 ChainTransfers::<T>::try_mutate(|transfers| {
376 Pallet::<T>::update_transfer_rejected(transfers, src_chain_id, amount)
377 })?;
378 }
379 }
380
381 response
382 }
383
384 fn message_weight(&self) -> Weight {
385 T::WeightInfo::message()
386 }
387
388 fn message_response(
389 &self,
390 dst_chain_id: ChainId,
391 message_id: MessageIdOf<T>,
392 req: EndpointRequest,
393 resp: EndpointResponse,
394 ) -> DispatchResult {
395 let transfer = OutgoingTransfers::<T>::take(dst_chain_id, message_id)
397 .ok_or(Error::<T>::MissingTransferRequest)?;
398 ensure!(
399 req.payload == transfer.encode(),
400 Error::<T>::InvalidTransferRequest
401 );
402
403 match resp {
405 Ok(_) => {
406 frame_system::Pallet::<T>::deposit_event(
408 Into::<<T as Config>::RuntimeEvent>::into(
409 Event::<T>::OutgoingTransferSuccessful {
410 chain_id: dst_chain_id,
411 message_id,
412 },
413 ),
414 );
415 }
416 Err(err) => {
417 let account_id =
420 T::AccountIdConverter::try_convert_back(transfer.sender.account_id)
421 .ok_or(Error::<T>::InvalidAccountId)?;
422
423 if T::SelfChainId::get().is_consensus_chain() {
426 Pallet::<T>::claim_rejected_transfer(
427 T::SelfChainId::get(),
428 dst_chain_id,
429 transfer.amount,
430 )?;
431 } else {
432 ChainTransfers::<T>::try_mutate(|transfers| {
433 Pallet::<T>::update_transfer_revert(
434 transfers,
435 dst_chain_id,
436 transfer.amount,
437 )
438 })?;
439 }
440
441 let _imbalance = T::Currency::deposit_creating(&account_id, transfer.amount);
442 frame_system::Pallet::<T>::deposit_event(
443 Into::<<T as Config>::RuntimeEvent>::into(
444 Event::<T>::OutgoingTransferFailed {
445 chain_id: dst_chain_id,
446 message_id,
447 err,
448 },
449 ),
450 );
451 }
452 }
453
454 Ok(())
455 }
456
457 fn message_response_weight(&self) -> Weight {
458 T::WeightInfo::message_response()
459 }
460 }
461}
462
463impl<T: Config> sp_domains::DomainsTransfersTracker<BalanceOf<T>> for Pallet<T> {
464 type Error = Error<T>;
465
466 fn initialize_domain_balance(
467 domain_id: DomainId,
468 amount: BalanceOf<T>,
469 ) -> Result<(), Self::Error> {
470 Self::ensure_consensus_chain()?;
471
472 ensure!(
473 !DomainBalances::<T>::contains_key(domain_id),
474 Error::DomainBalanceAlreadyInitialized
475 );
476
477 DomainBalances::<T>::set(domain_id, amount);
478 Ok(())
479 }
480
481 fn note_transfer(
482 from_chain_id: ChainId,
483 to_chain_id: ChainId,
484 amount: BalanceOf<T>,
485 ) -> Result<(), Self::Error> {
486 Self::ensure_consensus_chain()?;
487
488 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
489 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
490 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
491 *current_balance = current_balance
492 .checked_sub(&amount)
493 .ok_or(Error::LowBalanceOnDomain)?;
494 Ok(())
495 })?;
496 }
497
498 *total_amount = total_amount
499 .checked_add(&amount)
500 .ok_or(Error::BalanceOverflow)?;
501 Ok(())
502 })?;
503
504 Ok(())
505 }
506
507 fn confirm_transfer(
508 from_chain_id: ChainId,
509 to_chain_id: ChainId,
510 amount: BalanceOf<T>,
511 ) -> Result<(), Self::Error> {
512 Self::ensure_consensus_chain()?;
513 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
514 *total_amount = total_amount
515 .checked_sub(&amount)
516 .ok_or(Error::BalanceUnderflow)?;
517
518 if let Some(domain_id) = to_chain_id.maybe_domain_chain() {
519 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
520 *current_balance = current_balance
521 .checked_add(&amount)
522 .ok_or(Error::BalanceOverflow)?;
523 Ok(())
524 })?;
525 }
526
527 Ok(())
528 })?;
529
530 Ok(())
531 }
532
533 fn claim_rejected_transfer(
534 from_chain_id: ChainId,
535 to_chain_id: ChainId,
536 amount: BalanceOf<T>,
537 ) -> Result<(), Self::Error> {
538 Self::ensure_consensus_chain()?;
539 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
540 *total_amount = total_amount
541 .checked_sub(&amount)
542 .ok_or(Error::BalanceUnderflow)?;
543
544 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
545 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
546 *current_balance = current_balance
547 .checked_add(&amount)
548 .ok_or(Error::BalanceOverflow)?;
549 Ok(())
550 })?;
551 }
552
553 Ok(())
554 })?;
555
556 Ok(())
557 }
558
559 fn reject_transfer(
560 from_chain_id: ChainId,
561 to_chain_id: ChainId,
562 amount: BalanceOf<T>,
563 ) -> Result<(), Self::Error> {
564 Self::ensure_consensus_chain()?;
565 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
566 *total_amount = total_amount
567 .checked_sub(&amount)
568 .ok_or(Error::BalanceUnderflow)?;
569
570 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
571 *total_amount = total_amount
572 .checked_add(&amount)
573 .ok_or(Error::BalanceOverflow)?;
574 Ok(())
575 })?;
576
577 Ok(())
578 })?;
579
580 Ok(())
581 }
582
583 fn reduce_domain_balance(domain_id: DomainId, amount: BalanceOf<T>) -> Result<(), Self::Error> {
584 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
585 *current_balance = current_balance
586 .checked_sub(&amount)
587 .ok_or(Error::LowBalanceOnDomain)?;
588 Ok(())
589 })
590 }
591}
592
593impl<T: Config> NoteChainTransfer<BalanceOf<T>> for Pallet<T> {
594 fn note_transfer_in(amount: BalanceOf<T>, from_chain_id: ChainId) -> bool {
595 if T::SelfChainId::get().is_consensus_chain() {
596 Pallet::<T>::confirm_transfer(from_chain_id, T::SelfChainId::get(), amount).is_ok()
597 } else {
598 ChainTransfers::<T>::try_mutate(|transfers| {
599 Pallet::<T>::update_transfer_in(transfers, from_chain_id, amount)
600 })
601 .is_ok()
602 }
603 }
604
605 fn note_transfer_out(amount: BalanceOf<T>, to_chain_id: ChainId) -> bool {
606 if T::SelfChainId::get().is_consensus_chain() {
607 Self::note_transfer(T::SelfChainId::get(), to_chain_id, amount).is_ok()
608 } else {
609 ChainTransfers::<T>::try_mutate(|transfers| {
610 Self::update_transfer_out(transfers, to_chain_id, amount)
611 })
612 .is_ok()
613 }
614 }
615}
616
617impl<T: Config> Pallet<T> {
618 fn ensure_consensus_chain() -> Result<(), Error<T>> {
619 ensure!(
620 T::SelfChainId::get().is_consensus_chain(),
621 Error::NonConsensusChain
622 );
623
624 Ok(())
625 }
626
627 fn finalize_transfer(
628 src_chain_id: ChainId,
629 message_id: MessageIdOf<T>,
630 req: Transfer<BalanceOf<T>>,
631 ) -> EndpointResponse {
632 let account_id = T::AccountIdConverter::try_convert_back(req.receiver.account_id)
634 .ok_or(Error::<T>::InvalidAccountId)?;
635
636 if T::SelfChainId::get().is_consensus_chain() {
639 Pallet::<T>::confirm_transfer(src_chain_id, T::SelfChainId::get(), req.amount)?
640 } else {
641 ChainTransfers::<T>::try_mutate(|transfers| {
642 Pallet::<T>::update_transfer_in(transfers, src_chain_id, req.amount)
643 })?;
644 }
645
646 let _imbalance = T::Currency::deposit_creating(&account_id, req.amount);
647
648 frame_system::Pallet::<T>::deposit_event(Into::<<T as Config>::RuntimeEvent>::into(
649 Event::<T>::IncomingTransferSuccessful {
650 chain_id: src_chain_id,
651 message_id,
652 amount: req.amount,
653 },
654 ));
655 Ok(vec![])
656 }
657
658 fn update_transfer_out(
659 transfers: &mut Transfers<BalanceOf<T>>,
660 to_chain_id: ChainId,
661 amount: BalanceOf<T>,
662 ) -> DispatchResult {
663 let total_transfer =
664 if let Some(current_transfer_amount) = transfers.transfers_out.get(&to_chain_id) {
665 current_transfer_amount
666 .checked_add(&amount)
667 .ok_or(Error::<T>::BalanceOverflow)?
668 } else {
669 amount
670 };
671 transfers.transfers_out.insert(to_chain_id, total_transfer);
672 Ok(())
673 }
674
675 fn update_transfer_in(
676 transfers: &mut Transfers<BalanceOf<T>>,
677 from_chain_id: ChainId,
678 amount: BalanceOf<T>,
679 ) -> DispatchResult {
680 let total_transfer =
681 if let Some(current_transfer_amount) = transfers.transfers_in.get(&from_chain_id) {
682 current_transfer_amount
683 .checked_add(&amount)
684 .ok_or(Error::<T>::BalanceOverflow)?
685 } else {
686 amount
687 };
688 transfers.transfers_in.insert(from_chain_id, total_transfer);
689 Ok(())
690 }
691
692 fn update_transfer_revert(
693 transfers: &mut Transfers<BalanceOf<T>>,
694 to_chain_id: ChainId,
695 amount: BalanceOf<T>,
696 ) -> DispatchResult {
697 let total_transfer = if let Some(current_transfer_amount) =
698 transfers.rejected_transfers_claimed.get(&to_chain_id)
699 {
700 current_transfer_amount
701 .checked_add(&amount)
702 .ok_or(Error::<T>::BalanceOverflow)?
703 } else {
704 amount
705 };
706 transfers
707 .rejected_transfers_claimed
708 .insert(to_chain_id, total_transfer);
709 Ok(())
710 }
711
712 fn update_transfer_rejected(
713 transfers: &mut Transfers<BalanceOf<T>>,
714 from_chain_id: ChainId,
715 amount: BalanceOf<T>,
716 ) -> DispatchResult {
717 let total_transfer = if let Some(current_transfer_amount) =
718 transfers.transfers_rejected.get(&from_chain_id)
719 {
720 current_transfer_amount
721 .checked_add(&amount)
722 .ok_or(Error::<T>::BalanceOverflow)?
723 } else {
724 amount
725 };
726 transfers
727 .transfers_rejected
728 .insert(from_chain_id, total_transfer);
729 Ok(())
730 }
731}