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, DecodeWithMemTracking, 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]);
52const ZERO_SUBSTRATE_ADDRESS: MultiAccountId = MultiAccountId::AccountId32([0; 32]);
53
54#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo, DecodeWithMemTracking)]
56pub struct Location {
57 pub chain_id: ChainId,
59 pub account_id: MultiAccountId,
61}
62
63#[derive(Debug, Encode, Decode, Clone, Eq, PartialEq, TypeInfo)]
65pub struct Transfer<Balance> {
66 pub amount: Balance,
68 pub sender: Location,
70 pub receiver: Location,
72}
73
74pub type BalanceOf<T> =
76 <<T as Config>::Currency as Currency<<T as frame_system::Config>::AccountId>>::Balance;
77
78type MessageIdOf<T> = <<T as Config>::Sender as sp_messenger::endpoint::Sender<
79 <T as frame_system::Config>::AccountId,
80>>::MessageId;
81
82#[frame_support::pallet]
83mod pallet {
84 use crate::weights::WeightInfo;
85 use crate::{
86 BalanceOf, Location, MessageIdOf, MultiAccountId, Transfer, TryConvertBack,
87 ZERO_EVM_ADDRESS, ZERO_SUBSTRATE_ADDRESS,
88 };
89 #[cfg(not(feature = "std"))]
90 use alloc::vec::Vec;
91 use frame_support::pallet_prelude::*;
92 use frame_support::traits::{Currency, ExistenceRequirement, WithdrawReasons};
93 use frame_support::weights::Weight;
94 use frame_system::pallet_prelude::*;
95 use parity_scale_codec::{Decode, Encode};
96 use sp_domains::execution_receipt::Transfers;
97 use sp_domains::{DomainId, DomainsTransfersTracker};
98 use sp_messenger::endpoint::{
99 Endpoint, EndpointHandler as EndpointHandlerT, EndpointId, EndpointRequest,
100 EndpointResponse, Sender,
101 };
102 use sp_messenger::messages::ChainId;
103 use sp_runtime::traits::Convert;
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 ensure!(
270 dst_location.account_id != ZERO_SUBSTRATE_ADDRESS,
271 Error::<T>::InvalidAccountId
272 );
273
274 let _imbalance = T::Currency::withdraw(
276 &sender,
277 amount,
278 WithdrawReasons::TRANSFER,
279 ExistenceRequirement::KeepAlive,
280 )
281 .map_err(|_| Error::<T>::LowBalance)?;
282
283 let dst_chain_id = dst_location.chain_id;
285 let transfer = Transfer {
286 amount,
287 sender: Location {
288 chain_id: T::SelfChainId::get(),
289 account_id: T::AccountIdConverter::convert(sender.clone()),
290 },
291 receiver: dst_location,
292 };
293
294 let message_id = T::Sender::send_message(
296 &sender,
297 dst_chain_id,
298 EndpointRequest {
299 src_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
300 dst_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
302 payload: transfer.encode(),
303 },
304 )?;
305
306 OutgoingTransfers::<T>::insert(dst_chain_id, message_id, transfer);
307 Self::deposit_event(Event::<T>::OutgoingTransferInitiated {
308 chain_id: dst_chain_id,
309 message_id,
310 amount,
311 });
312
313 if T::SelfChainId::get().is_consensus_chain() {
316 Self::note_transfer(T::SelfChainId::get(), dst_chain_id, amount)?
317 } else {
318 ChainTransfers::<T>::try_mutate(|transfers| {
319 Self::update_transfer_out(transfers, dst_chain_id, amount)
320 })?;
321 }
322
323 Ok(())
324 }
325 }
326
327 #[pallet::hooks]
328 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
329 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
330 ChainTransfers::<T>::set(Default::default());
334 T::DbWeight::get().writes(1)
335 }
336 }
337
338 impl<T: Config> Pallet<T> {
339 pub fn transfers_storage_key() -> Vec<u8> {
340 use frame_support::storage::generator::StorageValue;
341 ChainTransfers::<T>::storage_value_final_key().to_vec()
342 }
343 }
344
345 #[derive(Debug)]
347 pub struct EndpointHandler<T>(pub PhantomData<T>);
348
349 impl<T: Config> EndpointHandlerT<MessageIdOf<T>> for EndpointHandler<T> {
350 fn message(
351 &self,
352 src_chain_id: ChainId,
353 message_id: MessageIdOf<T>,
354 req: EndpointRequest,
355 pre_check_result: DispatchResult,
356 ) -> EndpointResponse {
357 let dst_endpoint = req.dst_endpoint;
359 let req = match Transfer::decode(&mut req.payload.as_slice()) {
360 Ok(req) => req,
361 Err(_) => return Err(Error::<T>::InvalidPayload.into()),
362 };
363
364 let pre_check_handler = || {
365 ensure!(
367 T::SelfChainId::get() != src_chain_id,
368 Error::<T>::InvalidTransferRequest
369 );
370
371 ensure!(
373 dst_endpoint == Endpoint::Id(T::SelfEndpointId::get()),
374 Error::<T>::UnexpectedMessage
375 );
376
377 pre_check_result
378 };
379
380 let amount = req.amount;
381 let response = match pre_check_handler() {
382 Ok(_) => Pallet::<T>::finalize_transfer(src_chain_id, message_id, req),
383 Err(err) => Err(err),
384 };
385
386 if response.is_err() {
387 if T::SelfChainId::get().is_consensus_chain() {
390 Pallet::<T>::reject_transfer(src_chain_id, T::SelfChainId::get(), amount)?;
391 } else {
392 ChainTransfers::<T>::try_mutate(|transfers| {
393 Pallet::<T>::update_transfer_rejected(transfers, src_chain_id, amount)
394 })?;
395 }
396 }
397
398 response
399 }
400
401 fn message_weight(&self) -> Weight {
402 T::WeightInfo::message()
403 }
404
405 fn message_response(
406 &self,
407 dst_chain_id: ChainId,
408 message_id: MessageIdOf<T>,
409 req: EndpointRequest,
410 resp: EndpointResponse,
411 ) -> DispatchResult {
412 let transfer = OutgoingTransfers::<T>::take(dst_chain_id, message_id)
414 .ok_or(Error::<T>::MissingTransferRequest)?;
415 ensure!(
416 req.payload == transfer.encode(),
417 Error::<T>::InvalidTransferRequest
418 );
419
420 match resp {
422 Ok(_) => {
423 frame_system::Pallet::<T>::deposit_event(
425 Into::<<T as Config>::RuntimeEvent>::into(
426 Event::<T>::OutgoingTransferSuccessful {
427 chain_id: dst_chain_id,
428 message_id,
429 },
430 ),
431 );
432 }
433 Err(err) => {
434 let account_id =
437 T::AccountIdConverter::try_convert_back(transfer.sender.account_id)
438 .ok_or(Error::<T>::InvalidAccountId)?;
439
440 if T::SelfChainId::get().is_consensus_chain() {
443 Pallet::<T>::claim_rejected_transfer(
444 T::SelfChainId::get(),
445 dst_chain_id,
446 transfer.amount,
447 )?;
448 } else {
449 ChainTransfers::<T>::try_mutate(|transfers| {
450 Pallet::<T>::update_transfer_revert(
451 transfers,
452 dst_chain_id,
453 transfer.amount,
454 )
455 })?;
456 }
457
458 let _imbalance = T::Currency::deposit_creating(&account_id, transfer.amount);
459 frame_system::Pallet::<T>::deposit_event(
460 Into::<<T as Config>::RuntimeEvent>::into(
461 Event::<T>::OutgoingTransferFailed {
462 chain_id: dst_chain_id,
463 message_id,
464 err,
465 },
466 ),
467 );
468 }
469 }
470
471 Ok(())
472 }
473
474 fn message_response_weight(&self) -> Weight {
475 T::WeightInfo::message_response()
476 }
477 }
478}
479
480impl<T: Config> sp_domains::DomainsTransfersTracker<BalanceOf<T>> for Pallet<T> {
481 type Error = Error<T>;
482
483 fn initialize_domain_balance(
484 domain_id: DomainId,
485 amount: BalanceOf<T>,
486 ) -> Result<(), Self::Error> {
487 Self::ensure_consensus_chain()?;
488
489 ensure!(
490 !DomainBalances::<T>::contains_key(domain_id),
491 Error::DomainBalanceAlreadyInitialized
492 );
493
494 DomainBalances::<T>::set(domain_id, amount);
495 Ok(())
496 }
497
498 fn note_transfer(
499 from_chain_id: ChainId,
500 to_chain_id: ChainId,
501 amount: BalanceOf<T>,
502 ) -> Result<(), Self::Error> {
503 Self::ensure_consensus_chain()?;
504
505 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
506 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
507 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
508 *current_balance = current_balance
509 .checked_sub(&amount)
510 .ok_or(Error::LowBalanceOnDomain)?;
511 Ok(())
512 })?;
513 }
514
515 *total_amount = total_amount
516 .checked_add(&amount)
517 .ok_or(Error::BalanceOverflow)?;
518 Ok(())
519 })?;
520
521 Ok(())
522 }
523
524 fn confirm_transfer(
525 from_chain_id: ChainId,
526 to_chain_id: ChainId,
527 amount: BalanceOf<T>,
528 ) -> Result<(), Self::Error> {
529 Self::ensure_consensus_chain()?;
530 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
531 *total_amount = total_amount
532 .checked_sub(&amount)
533 .ok_or(Error::BalanceUnderflow)?;
534
535 if let Some(domain_id) = to_chain_id.maybe_domain_chain() {
536 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
537 *current_balance = current_balance
538 .checked_add(&amount)
539 .ok_or(Error::BalanceOverflow)?;
540 Ok(())
541 })?;
542 }
543
544 Ok(())
545 })?;
546
547 Ok(())
548 }
549
550 fn claim_rejected_transfer(
551 from_chain_id: ChainId,
552 to_chain_id: ChainId,
553 amount: BalanceOf<T>,
554 ) -> Result<(), Self::Error> {
555 Self::ensure_consensus_chain()?;
556 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
557 *total_amount = total_amount
558 .checked_sub(&amount)
559 .ok_or(Error::BalanceUnderflow)?;
560
561 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
562 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
563 *current_balance = current_balance
564 .checked_add(&amount)
565 .ok_or(Error::BalanceOverflow)?;
566 Ok(())
567 })?;
568 }
569
570 Ok(())
571 })?;
572
573 Ok(())
574 }
575
576 fn reject_transfer(
577 from_chain_id: ChainId,
578 to_chain_id: ChainId,
579 amount: BalanceOf<T>,
580 ) -> Result<(), Self::Error> {
581 Self::ensure_consensus_chain()?;
582 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
583 *total_amount = total_amount
584 .checked_sub(&amount)
585 .ok_or(Error::BalanceUnderflow)?;
586
587 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
588 *total_amount = total_amount
589 .checked_add(&amount)
590 .ok_or(Error::BalanceOverflow)?;
591 Ok(())
592 })?;
593
594 Ok(())
595 })?;
596
597 Ok(())
598 }
599
600 fn reduce_domain_balance(domain_id: DomainId, amount: BalanceOf<T>) -> Result<(), Self::Error> {
601 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
602 *current_balance = current_balance
603 .checked_sub(&amount)
604 .ok_or(Error::LowBalanceOnDomain)?;
605 Ok(())
606 })
607 }
608}
609
610impl<T: Config> NoteChainTransfer<BalanceOf<T>> for Pallet<T> {
611 fn note_transfer_in(amount: BalanceOf<T>, from_chain_id: ChainId) -> bool {
612 if T::SelfChainId::get().is_consensus_chain() {
613 Pallet::<T>::confirm_transfer(from_chain_id, T::SelfChainId::get(), amount).is_ok()
614 } else {
615 ChainTransfers::<T>::try_mutate(|transfers| {
616 Pallet::<T>::update_transfer_in(transfers, from_chain_id, amount)
617 })
618 .is_ok()
619 }
620 }
621
622 fn note_transfer_out(amount: BalanceOf<T>, to_chain_id: ChainId) -> bool {
623 if T::SelfChainId::get().is_consensus_chain() {
624 Self::note_transfer(T::SelfChainId::get(), to_chain_id, amount).is_ok()
625 } else {
626 ChainTransfers::<T>::try_mutate(|transfers| {
627 Self::update_transfer_out(transfers, to_chain_id, amount)
628 })
629 .is_ok()
630 }
631 }
632}
633
634impl<T: Config> Pallet<T> {
635 fn ensure_consensus_chain() -> Result<(), Error<T>> {
636 ensure!(
637 T::SelfChainId::get().is_consensus_chain(),
638 Error::NonConsensusChain
639 );
640
641 Ok(())
642 }
643
644 fn finalize_transfer(
645 src_chain_id: ChainId,
646 message_id: MessageIdOf<T>,
647 req: Transfer<BalanceOf<T>>,
648 ) -> EndpointResponse {
649 let account_id = T::AccountIdConverter::try_convert_back(req.receiver.account_id)
651 .ok_or(Error::<T>::InvalidAccountId)?;
652
653 if T::SelfChainId::get().is_consensus_chain() {
656 Pallet::<T>::confirm_transfer(src_chain_id, T::SelfChainId::get(), req.amount)?
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}