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