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