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<RuntimeEvent: From<Event<Self>>> {
107 type SelfChainId: Get<ChainId>;
109
110 type SelfEndpointId: Get<EndpointId>;
112
113 type Currency: Currency<Self::AccountId>;
115
116 type Sender: Sender<Self::AccountId>;
118
119 type AccountIdConverter: TryConvertBack<Self::AccountId, MultiAccountId>;
121
122 type WeightInfo: WeightInfo;
124
125 type MinimumTransfer: Get<BalanceOf<Self>>;
127 }
128
129 #[pallet::pallet]
131 #[pallet::without_storage_info]
132 pub struct Pallet<T>(_);
133
134 #[pallet::storage]
136 #[pallet::getter(fn outgoing_transfers)]
137 pub(super) type OutgoingTransfers<T: Config> = StorageDoubleMap<
138 _,
139 Identity,
140 ChainId,
141 Identity,
142 MessageIdOf<T>,
143 Transfer<BalanceOf<T>>,
144 OptionQuery,
145 >;
146
147 #[pallet::storage]
149 #[pallet::getter(fn domain_balances)]
150 pub(super) type DomainBalances<T: Config> =
151 StorageMap<_, Identity, DomainId, BalanceOf<T>, ValueQuery>;
152
153 #[pallet::storage]
156 #[pallet::getter(fn chain_transfers)]
157 pub(super) type ChainTransfers<T: Config> =
158 StorageValue<_, Transfers<BalanceOf<T>>, ValueQuery>;
159
160 #[pallet::storage]
162 #[pallet::getter(fn unconfirmed_transfers)]
163 pub(super) type UnconfirmedTransfers<T: Config> =
164 StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
165
166 #[pallet::storage]
168 #[pallet::getter(fn cancelled_transfers)]
169 pub(super) type CancelledTransfers<T: Config> =
170 StorageDoubleMap<_, Identity, ChainId, Identity, ChainId, BalanceOf<T>, ValueQuery>;
171
172 #[pallet::event]
174 #[pallet::generate_deposit(pub (super) fn deposit_event)]
175 pub enum Event<T: Config> {
176 OutgoingTransferInitiated {
178 chain_id: ChainId,
180 message_id: MessageIdOf<T>,
182 amount: BalanceOf<T>,
184 },
185
186 OutgoingTransferFailed {
188 chain_id: ChainId,
190 message_id: MessageIdOf<T>,
192 err: DispatchError,
194 },
195
196 OutgoingTransferSuccessful {
198 chain_id: ChainId,
200 message_id: MessageIdOf<T>,
202 },
203
204 IncomingTransferSuccessful {
206 chain_id: ChainId,
208 message_id: MessageIdOf<T>,
210 amount: BalanceOf<T>,
212 },
213 }
214
215 #[pallet::error]
217 pub enum Error<T> {
218 LowBalance,
220 InvalidPayload,
222 MissingTransferRequest,
224 InvalidTransferRequest,
226 UnexpectedMessage,
228 InvalidAccountId,
230 LowBalanceOnDomain,
232 NonConsensusChain,
234 BalanceOverflow,
236 BalanceUnderflow,
238 DomainBalanceAlreadyInitialized,
240 MinimumTransferAmount,
242 }
243
244 #[pallet::call]
245 impl<T: Config> Pallet<T> {
246 #[pallet::call_index(0)]
249 #[pallet::weight(T::WeightInfo::transfer())]
250 pub fn transfer(
251 origin: OriginFor<T>,
252 dst_location: Location,
253 amount: BalanceOf<T>,
254 ) -> DispatchResult {
255 let sender = ensure_signed(origin)?;
256 ensure!(
257 amount >= T::MinimumTransfer::get(),
258 Error::<T>::MinimumTransferAmount
259 );
260
261 ensure!(
262 dst_location.account_id != ZERO_EVM_ADDRESS,
263 Error::<T>::InvalidAccountId
264 );
265
266 ensure!(
267 dst_location.account_id != ZERO_SUBSTRATE_ADDRESS,
268 Error::<T>::InvalidAccountId
269 );
270
271 let _imbalance = T::Currency::withdraw(
273 &sender,
274 amount,
275 WithdrawReasons::TRANSFER,
276 ExistenceRequirement::KeepAlive,
277 )
278 .map_err(|_| Error::<T>::LowBalance)?;
279
280 let dst_chain_id = dst_location.chain_id;
282 let transfer = Transfer {
283 amount,
284 sender: Location {
285 chain_id: T::SelfChainId::get(),
286 account_id: T::AccountIdConverter::convert(sender.clone()),
287 },
288 receiver: dst_location,
289 };
290
291 let message_id = T::Sender::send_message(
293 &sender,
294 dst_chain_id,
295 EndpointRequest {
296 src_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
297 dst_endpoint: Endpoint::Id(T::SelfEndpointId::get()),
299 payload: transfer.encode(),
300 },
301 )?;
302
303 OutgoingTransfers::<T>::insert(dst_chain_id, message_id, transfer);
304 Self::deposit_event(Event::<T>::OutgoingTransferInitiated {
305 chain_id: dst_chain_id,
306 message_id,
307 amount,
308 });
309
310 if T::SelfChainId::get().is_consensus_chain() {
313 Self::note_transfer(T::SelfChainId::get(), dst_chain_id, amount)?
314 } else {
315 ChainTransfers::<T>::try_mutate(|transfers| {
316 Self::update_transfer_out(transfers, dst_chain_id, amount)
317 })?;
318 }
319
320 Ok(())
321 }
322 }
323
324 #[pallet::hooks]
325 impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
326 fn on_initialize(_n: BlockNumberFor<T>) -> Weight {
327 ChainTransfers::<T>::set(Default::default());
331 T::DbWeight::get().writes(1)
332 }
333 }
334
335 impl<T: Config> Pallet<T> {
336 pub fn transfers_storage_key() -> Vec<u8> {
337 use frame_support::storage::generator::StorageValue;
338 ChainTransfers::<T>::storage_value_final_key().to_vec()
339 }
340 }
341
342 #[derive(Debug)]
344 pub struct EndpointHandler<T>(pub PhantomData<T>);
345
346 impl<T: Config> EndpointHandlerT<MessageIdOf<T>> for EndpointHandler<T> {
347 fn message(
348 &self,
349 src_chain_id: ChainId,
350 message_id: MessageIdOf<T>,
351 req: EndpointRequest,
352 pre_check_result: DispatchResult,
353 ) -> EndpointResponse {
354 let dst_endpoint = req.dst_endpoint;
356 let req = match Transfer::decode(&mut req.payload.as_slice()) {
357 Ok(req) => req,
358 Err(_) => return Err(Error::<T>::InvalidPayload.into()),
359 };
360
361 let pre_check_handler = || {
362 ensure!(
364 T::SelfChainId::get() != src_chain_id,
365 Error::<T>::InvalidTransferRequest
366 );
367
368 ensure!(
370 dst_endpoint == Endpoint::Id(T::SelfEndpointId::get()),
371 Error::<T>::UnexpectedMessage
372 );
373
374 pre_check_result
375 };
376
377 let amount = req.amount;
378 let response = match pre_check_handler() {
379 Ok(_) => Pallet::<T>::finalize_transfer(src_chain_id, message_id, req),
380 Err(err) => Err(err),
381 };
382
383 if response.is_err() {
384 if T::SelfChainId::get().is_consensus_chain() {
387 Pallet::<T>::reject_transfer(src_chain_id, T::SelfChainId::get(), amount)?;
388 } else {
389 ChainTransfers::<T>::try_mutate(|transfers| {
390 Pallet::<T>::update_transfer_rejected(transfers, src_chain_id, amount)
391 })?;
392 }
393 }
394
395 response
396 }
397
398 fn message_weight(&self) -> Weight {
399 T::WeightInfo::message()
400 }
401
402 fn message_response(
403 &self,
404 dst_chain_id: ChainId,
405 message_id: MessageIdOf<T>,
406 req: EndpointRequest,
407 resp: EndpointResponse,
408 ) -> DispatchResult {
409 let transfer = OutgoingTransfers::<T>::take(dst_chain_id, message_id)
411 .ok_or(Error::<T>::MissingTransferRequest)?;
412 ensure!(
413 req.payload == transfer.encode(),
414 Error::<T>::InvalidTransferRequest
415 );
416
417 match resp {
419 Ok(_) => {
420 frame_system::Pallet::<T>::deposit_event(Into::<
422 <T as frame_system::Config>::RuntimeEvent,
423 >::into(
424 Event::<T>::OutgoingTransferSuccessful {
425 chain_id: dst_chain_id,
426 message_id,
427 },
428 ));
429 }
430 Err(err) => {
431 let account_id =
434 T::AccountIdConverter::try_convert_back(transfer.sender.account_id)
435 .ok_or(Error::<T>::InvalidAccountId)?;
436
437 if T::SelfChainId::get().is_consensus_chain() {
440 Pallet::<T>::claim_rejected_transfer(
441 T::SelfChainId::get(),
442 dst_chain_id,
443 transfer.amount,
444 )?;
445 } else {
446 ChainTransfers::<T>::try_mutate(|transfers| {
447 Pallet::<T>::update_transfer_revert(
448 transfers,
449 dst_chain_id,
450 transfer.amount,
451 )
452 })?;
453 }
454
455 let _imbalance = T::Currency::deposit_creating(&account_id, transfer.amount);
456 frame_system::Pallet::<T>::deposit_event(Into::<
457 <T as frame_system::Config>::RuntimeEvent,
458 >::into(
459 Event::<T>::OutgoingTransferFailed {
460 chain_id: dst_chain_id,
461 message_id,
462 err,
463 },
464 ));
465 }
466 }
467
468 Ok(())
469 }
470
471 fn message_response_weight(&self) -> Weight {
472 T::WeightInfo::message_response()
473 }
474 }
475}
476
477impl<T: Config> sp_domains::DomainsTransfersTracker<BalanceOf<T>> for Pallet<T> {
478 type Error = Error<T>;
479
480 fn initialize_domain_balance(
481 domain_id: DomainId,
482 amount: BalanceOf<T>,
483 ) -> Result<(), Self::Error> {
484 Self::ensure_consensus_chain()?;
485
486 ensure!(
487 !DomainBalances::<T>::contains_key(domain_id),
488 Error::DomainBalanceAlreadyInitialized
489 );
490
491 DomainBalances::<T>::set(domain_id, amount);
492 Ok(())
493 }
494
495 fn note_transfer(
496 from_chain_id: ChainId,
497 to_chain_id: ChainId,
498 amount: BalanceOf<T>,
499 ) -> Result<(), Self::Error> {
500 Self::ensure_consensus_chain()?;
501
502 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
503 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
504 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
505 *current_balance = current_balance
506 .checked_sub(&amount)
507 .ok_or(Error::LowBalanceOnDomain)?;
508 Ok(())
509 })?;
510 }
511
512 *total_amount = total_amount
513 .checked_add(&amount)
514 .ok_or(Error::BalanceOverflow)?;
515 Ok(())
516 })?;
517
518 Ok(())
519 }
520
521 fn confirm_transfer(
522 from_chain_id: ChainId,
523 to_chain_id: ChainId,
524 amount: BalanceOf<T>,
525 ) -> Result<(), Self::Error> {
526 Self::ensure_consensus_chain()?;
527 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
528 *total_amount = total_amount
529 .checked_sub(&amount)
530 .ok_or(Error::BalanceUnderflow)?;
531
532 if let Some(domain_id) = to_chain_id.maybe_domain_chain() {
533 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
534 *current_balance = current_balance
535 .checked_add(&amount)
536 .ok_or(Error::BalanceOverflow)?;
537 Ok(())
538 })?;
539 }
540
541 Ok(())
542 })?;
543
544 Ok(())
545 }
546
547 fn claim_rejected_transfer(
548 from_chain_id: ChainId,
549 to_chain_id: ChainId,
550 amount: BalanceOf<T>,
551 ) -> Result<(), Self::Error> {
552 Self::ensure_consensus_chain()?;
553 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
554 *total_amount = total_amount
555 .checked_sub(&amount)
556 .ok_or(Error::BalanceUnderflow)?;
557
558 if let Some(domain_id) = from_chain_id.maybe_domain_chain() {
559 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
560 *current_balance = current_balance
561 .checked_add(&amount)
562 .ok_or(Error::BalanceOverflow)?;
563 Ok(())
564 })?;
565 }
566
567 Ok(())
568 })?;
569
570 Ok(())
571 }
572
573 fn reject_transfer(
574 from_chain_id: ChainId,
575 to_chain_id: ChainId,
576 amount: BalanceOf<T>,
577 ) -> Result<(), Self::Error> {
578 Self::ensure_consensus_chain()?;
579 UnconfirmedTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
580 *total_amount = total_amount
581 .checked_sub(&amount)
582 .ok_or(Error::BalanceUnderflow)?;
583
584 CancelledTransfers::<T>::try_mutate(from_chain_id, to_chain_id, |total_amount| {
585 *total_amount = total_amount
586 .checked_add(&amount)
587 .ok_or(Error::BalanceOverflow)?;
588 Ok(())
589 })?;
590
591 Ok(())
592 })?;
593
594 Ok(())
595 }
596
597 fn reduce_domain_balance(domain_id: DomainId, amount: BalanceOf<T>) -> Result<(), Self::Error> {
598 DomainBalances::<T>::try_mutate(domain_id, |current_balance| {
599 *current_balance = current_balance
600 .checked_sub(&amount)
601 .ok_or(Error::LowBalanceOnDomain)?;
602 Ok(())
603 })
604 }
605}
606
607impl<T: Config> NoteChainTransfer<BalanceOf<T>> for Pallet<T> {
608 fn note_transfer_in(amount: BalanceOf<T>, from_chain_id: ChainId) -> bool {
609 if T::SelfChainId::get().is_consensus_chain() {
610 Pallet::<T>::confirm_transfer(from_chain_id, T::SelfChainId::get(), amount).is_ok()
611 } else {
612 ChainTransfers::<T>::try_mutate(|transfers| {
613 Pallet::<T>::update_transfer_in(transfers, from_chain_id, amount)
614 })
615 .is_ok()
616 }
617 }
618
619 fn note_transfer_out(amount: BalanceOf<T>, to_chain_id: ChainId) -> bool {
620 if T::SelfChainId::get().is_consensus_chain() {
621 Self::note_transfer(T::SelfChainId::get(), to_chain_id, amount).is_ok()
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 Pallet::<T>::confirm_transfer(src_chain_id, T::SelfChainId::get(), req.amount)?
654 } else {
655 ChainTransfers::<T>::try_mutate(|transfers| {
656 Pallet::<T>::update_transfer_in(transfers, src_chain_id, req.amount)
657 })?;
658 }
659
660 let _imbalance = T::Currency::deposit_creating(&account_id, req.amount);
661
662 frame_system::Pallet::<T>::deposit_event(
663 Into::<<T as frame_system::Config>::RuntimeEvent>::into(
664 Event::<T>::IncomingTransferSuccessful {
665 chain_id: src_chain_id,
666 message_id,
667 amount: req.amount,
668 },
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}