// Copyright 2019-2021 Parity Technologies (UK) Ltd. // This file is part of Parity Bridges Common. // Parity Bridges Common is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // Parity Bridges Common is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // You should have received a copy of the GNU General Public License // along with Parity Bridges Common. If not, see . //! Runtime module that allows token swap between two parties acting on different chains. //! //! The swap is made using message lanes between This (where `pallet-bridge-token-swap` pallet //! is deployed) and some other Bridged chain. No other assumptions about the Bridged chain are //! made, so we don't need it to have an instance of the `pallet-bridge-token-swap` pallet deployed. //! //! There are four accounts participating in the swap: //! //! 1) account of This chain that has signed the `create_swap` transaction and has balance on This //! chain. We'll be referring to this account as `source_account_at_this_chain`; //! //! 2) account of the Bridged chain that is sending the `claim_swap` message from the Bridged to //! This chain. This account has balance on Bridged chain and is willing to swap these tokens to //! This chain tokens of the `source_account_at_this_chain`. We'll be referring to this account //! as `target_account_at_bridged_chain`; //! //! 3) account of the Bridged chain that is indirectly controlled by the //! `source_account_at_this_chain`. We'll be referring this account as //! `source_account_at_bridged_chain`; //! //! 4) account of This chain that is indirectly controlled by the `target_account_at_bridged_chain`. //! We'll be referring this account as `target_account_at_this_chain`. //! //! So the tokens swap is an intention of `source_account_at_this_chain` to swap his //! `source_balance_at_this_chain` tokens to the `target_balance_at_bridged_chain` tokens owned by //! `target_account_at_bridged_chain`. The swap process goes as follows: //! //! 1) the `source_account_at_this_chain` account submits the `create_swap` transaction on This //! chain; //! //! 2) the tokens transfer message that would transfer `target_balance_at_bridged_chain` //! tokens from the `target_account_at_bridged_chain` to the `source_account_at_bridged_chain`, //! is sent over the bridge; //! //! 3) when transfer message is delivered and dispatched, the pallet receives notification; //! //! 4) if message has been successfully dispatched, the `target_account_at_bridged_chain` sends the //! message that would transfer `source_balance_at_this_chain` tokens to his //! `target_account_at_this_chain` account; //! //! 5) if message dispatch has failed, the `source_account_at_this_chain` may submit the //! `cancel_swap` transaction and return his `source_balance_at_this_chain` back to his account. //! //! While swap is pending, the `source_balance_at_this_chain` tokens are owned by the special //! temporary `swap_account_at_this_chain` account. It is destroyed upon swap completion. #![cfg_attr(not(feature = "std"), no_std)] use bp_messages::{ source_chain::{MessagesBridge, OnDeliveryConfirmed}, DeliveredMessages, LaneId, MessageNonce, }; use bp_runtime::{messages::DispatchFeePayment, ChainId}; use bp_token_swap::{ RawBridgedTransferCall, TokenSwap, TokenSwapCreation, TokenSwapState, TokenSwapType, }; use codec::{Decode, Encode}; use frame_support::{ fail, traits::{Currency, ExistenceRequirement}, weights::PostDispatchInfo, RuntimeDebug, }; use scale_info::TypeInfo; use sp_core::H256; use sp_io::hashing::blake2_256; use sp_runtime::traits::{Convert, Saturating}; use sp_std::{boxed::Box, marker::PhantomData}; use weights::WeightInfo; pub use weights_ext::WeightInfoExt; #[cfg(test)] mod mock; #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; pub mod weights; pub mod weights_ext; pub use pallet::*; /// Name of the `PendingSwaps` storage map. pub const PENDING_SWAPS_MAP_NAME: &str = "PendingSwaps"; /// Origin for the token swap pallet. #[derive(PartialEq, Eq, Clone, RuntimeDebug, Encode, Decode, TypeInfo)] pub enum RawOrigin { /// The call is originated by the token swap account. TokenSwap { /// Id of the account that has started the swap. source_account_at_this_chain: AccountId, /// Id of the account that holds the funds during this swap. The message fee is paid from /// this account funds. swap_account_at_this_chain: AccountId, }, /// Dummy to manage the fact we have instancing. _Phantom(PhantomData), } // comes from #[pallet::event] #[allow(clippy::unused_unit)] #[frame_support::pallet] pub mod pallet { use super::*; use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; #[pallet::config] pub trait Config: frame_system::Config { /// The overarching event type. type Event: From> + IsType<::Event>; /// Benchmarks results from runtime we're plugged into. type WeightInfo: WeightInfoExt; /// Id of the bridge with the Bridged chain. type BridgedChainId: Get; /// The identifier of outbound message lane on This chain used to send token transfer /// messages to the Bridged chain. /// /// It is highly recommended to use dedicated lane for every instance of token swap /// pallet. Messages delivery confirmation callback is implemented in the way that /// for every confirmed message, there is (at least) a storage read. Which mean, /// that if pallet will see unrelated confirmations, it'll just burn storage-read /// weight, achieving nothing. type OutboundMessageLaneId: Get; /// Messages bridge with Bridged chain. type MessagesBridge: MessagesBridge< Self::Origin, Self::AccountId, >::Balance, MessagePayloadOf, >; /// This chain Currency used in the tokens swap. type ThisCurrency: Currency; /// Converter from raw hash (derived from swap) to This chain account. type FromSwapToThisAccountIdConverter: Convert; /// The chain we're bridged to. type BridgedChain: bp_runtime::Chain; /// Converter from raw hash (derived from Bridged chain account) to This chain account. type FromBridgedToThisAccountIdConverter: Convert; } /// Tokens balance at This chain. pub type ThisChainBalance = <>::ThisCurrency as Currency< ::AccountId, >>::Balance; /// Type of the Bridged chain. pub type BridgedChainOf = >::BridgedChain; /// Tokens balance type at the Bridged chain. pub type BridgedBalanceOf = bp_runtime::BalanceOf>; /// Account identifier type at the Bridged chain. pub type BridgedAccountIdOf = bp_runtime::AccountIdOf>; /// Account public key type at the Bridged chain. pub type BridgedAccountPublicOf = bp_runtime::AccountPublicOf>; /// Account signature type at the Bridged chain. pub type BridgedAccountSignatureOf = bp_runtime::SignatureOf>; /// Bridge message payload used by the pallet. pub type MessagePayloadOf = bp_message_dispatch::MessagePayload< ::AccountId, BridgedAccountPublicOf, BridgedAccountSignatureOf, RawBridgedTransferCall, >; /// Type of `TokenSwap` used by the pallet. pub type TokenSwapOf = TokenSwap< BlockNumberFor, ThisChainBalance, ::AccountId, BridgedBalanceOf, BridgedAccountIdOf, >; /// Type of `TokenSwapCreation` used by the pallet. pub type TokenSwapCreationOf = TokenSwapCreation< BridgedAccountPublicOf, ThisChainBalance, BridgedAccountSignatureOf, >; #[pallet::pallet] #[pallet::generate_store(pub(super) trait Store)] #[pallet::without_storage_info] pub struct Pallet(PhantomData<(T, I)>); #[pallet::hooks] impl, I: 'static> Hooks> for Pallet {} #[pallet::call] impl, I: 'static> Pallet where BridgedAccountPublicOf: Parameter, Origin: Into, { /// Start token swap procedure. /// /// The dispatch origin for this call must be exactly the /// `swap.source_account_at_this_chain` account. /// /// Method arguments are: /// /// - `swap` - token swap intention; /// - `swap_creation_params` - additional parameters required to start tokens swap. /// /// The `source_account_at_this_chain` MUST have enough balance to cover both token swap and /// message transfer. Message fee may be estimated using corresponding `OutboundLaneApi` of /// This runtime. /// /// **WARNING**: the submitter of this transaction is responsible for verifying: /// /// 1) that the `swap_creation_params.bridged_currency_transfer` represents a valid token /// transfer call that transfers `swap.target_balance_at_bridged_chain` to his /// `swap.source_account_at_bridged_chain` account; /// /// 2) that either the `swap.source_account_at_bridged_chain` already exists, or the /// `swap.target_balance_at_bridged_chain` is above existential deposit of the Bridged /// chain; /// /// 3) the `swap_creation_params.target_public_at_bridged_chain` matches the /// `swap.target_account_at_bridged_chain`; /// /// 4) the `bridged_currency_transfer_signature` is valid and generated by the owner of /// the `swap_creation_params.target_public_at_bridged_chain` account (read more /// about [`CallOrigin::TargetAccount`]). /// /// Violating rule#1 will lead to losing your `source_balance_at_this_chain` tokens. /// Violating other rules will lead to losing message fees for this and other transactions + /// losing fees for message transfer. #[allow(clippy::boxed_local)] #[pallet::weight( T::WeightInfo::create_swap() .saturating_add(T::WeightInfo::send_message_weight( &&swap_creation_params.bridged_currency_transfer[..], T::DbWeight::get(), )) )] pub fn create_swap( origin: OriginFor, swap: TokenSwapOf, swap_creation_params: Box>, ) -> DispatchResultWithPostInfo { let TokenSwapCreation { target_public_at_bridged_chain, swap_delivery_and_dispatch_fee, bridged_chain_spec_version, bridged_currency_transfer, bridged_currency_transfer_weight, bridged_currency_transfer_signature, } = *swap_creation_params; // ensure that the `origin` is the same account that is mentioned in the `swap` // intention let origin_account = ensure_signed(origin)?; ensure!( origin_account == swap.source_account_at_this_chain, Error::::MismatchedSwapSourceOrigin, ); // remember weight components let base_weight = T::WeightInfo::create_swap(); // we can't exchange less than existential deposit (the temporary `swap_account` account // won't be created then) // // the same can also happen with the `swap.bridged_balance`, but we can't check it // here (without additional knowledge of the Bridged chain). So it is the `origin` // responsibility to check that the swap is valid. ensure!( swap.source_balance_at_this_chain >= T::ThisCurrency::minimum_balance(), Error::::TooLowBalanceOnThisChain, ); // if the swap is replay-protected, then we need to ensure that we have not yet passed // the specified block yet match swap.swap_type { TokenSwapType::TemporaryTargetAccountAtBridgedChain => (), TokenSwapType::LockClaimUntilBlock(block_number, _) => ensure!( block_number >= frame_system::Pallet::::block_number(), Error::::SwapPeriodIsFinished, ), } let swap_account = swap_account_id::(&swap); let actual_send_message_weight = frame_support::storage::with_transaction( || -> sp_runtime::TransactionOutcome> { // funds are transferred from This account to the temporary Swap account let transfer_result = T::ThisCurrency::transfer( &swap.source_account_at_this_chain, &swap_account, // saturating_add is ok, or we have the chain where single holder owns all // tokens swap.source_balance_at_this_chain .saturating_add(swap_delivery_and_dispatch_fee), // if we'll allow account to die, then he'll be unable to `cancel_claim` // if something won't work ExistenceRequirement::KeepAlive, ); if let Err(err) = transfer_result { log::error!( target: "runtime::bridge-token-swap", "Failed to transfer This chain tokens for the swap {:?} to Swap account ({:?}): {:?}", swap, swap_account, err, ); return sp_runtime::TransactionOutcome::Rollback(Err( Error::::FailedToTransferToSwapAccount.into(), )) } // the transfer message is sent over the bridge. The message is supposed to be a // `Currency::transfer` call on the bridged chain, but no checks are made - it // is the transaction submitter to ensure it is valid. let send_message_result = T::MessagesBridge::send_message( RawOrigin::TokenSwap { source_account_at_this_chain: swap.source_account_at_this_chain.clone(), swap_account_at_this_chain: swap_account.clone(), } .into(), T::OutboundMessageLaneId::get(), bp_message_dispatch::MessagePayload { spec_version: bridged_chain_spec_version, weight: bridged_currency_transfer_weight, origin: bp_message_dispatch::CallOrigin::TargetAccount( swap_account, target_public_at_bridged_chain, bridged_currency_transfer_signature, ), dispatch_fee_payment: DispatchFeePayment::AtTargetChain, call: bridged_currency_transfer, }, swap_delivery_and_dispatch_fee, ); let sent_message = match send_message_result { Ok(sent_message) => sent_message, Err(err) => { log::error!( target: "runtime::bridge-token-swap", "Failed to send token transfer message for swap {:?} to the Bridged chain: {:?}", swap, err, ); return sp_runtime::TransactionOutcome::Rollback(Err( Error::::FailedToSendTransferMessage.into(), )) }, }; // remember that we have started the swap let swap_hash = swap.using_encoded(blake2_256).into(); let insert_swap_result = PendingSwaps::::try_mutate(swap_hash, |maybe_state| { if maybe_state.is_some() { return Err(()) } *maybe_state = Some(TokenSwapState::Started); Ok(()) }); if insert_swap_result.is_err() { log::error!( target: "runtime::bridge-token-swap", "Failed to start token swap {:?}: the swap is already started", swap, ); return sp_runtime::TransactionOutcome::Rollback(Err( Error::::SwapAlreadyStarted.into(), )) } log::trace!( target: "runtime::bridge-token-swap", "The swap {:?} (hash {:?}) has been started", swap, swap_hash, ); // remember that we're waiting for the transfer message delivery confirmation PendingMessages::::insert(sent_message.nonce, swap_hash); // finally - emit the event Self::deposit_event(Event::SwapStarted(swap_hash, sent_message.nonce)); sp_runtime::TransactionOutcome::Commit(Ok(sent_message.weight)) }, )?; Ok(PostDispatchInfo { actual_weight: Some(base_weight.saturating_add(actual_send_message_weight)), pays_fee: Pays::Yes, }) } /// Claim previously reserved `source_balance_at_this_chain` by /// `target_account_at_this_chain`. /// /// **WARNING**: the correct way to call this function is to call it over the messages /// bridge with dispatch origin set to /// `pallet_bridge_dispatch::CallOrigin::SourceAccount(target_account_at_bridged_chain)`. /// /// This should be called only when successful transfer confirmation has been received. #[pallet::weight(T::WeightInfo::claim_swap())] pub fn claim_swap( origin: OriginFor, swap: TokenSwapOf, ) -> DispatchResultWithPostInfo { // ensure that the `origin` is controlled by the `swap.target_account_at_bridged_chain` let origin_account = ensure_signed(origin)?; let target_account_at_this_chain = target_account_at_this_chain::(&swap); ensure!(origin_account == target_account_at_this_chain, Error::::InvalidClaimant,); // ensure that the swap is confirmed let swap_hash = swap.using_encoded(blake2_256).into(); let swap_state = PendingSwaps::::get(swap_hash); match swap_state { Some(TokenSwapState::Started) => fail!(Error::::SwapIsPending), Some(TokenSwapState::Confirmed) => { let is_claim_allowed = match swap.swap_type { TokenSwapType::TemporaryTargetAccountAtBridgedChain => true, TokenSwapType::LockClaimUntilBlock(block_number, _) => block_number < frame_system::Pallet::::block_number(), }; ensure!(is_claim_allowed, Error::::SwapIsTemporaryLocked); }, Some(TokenSwapState::Failed) => fail!(Error::::SwapIsFailed), None => fail!(Error::::SwapIsInactive), } complete_claim::(swap, swap_hash, origin_account, Event::SwapClaimed(swap_hash)) } /// Return previously reserved `source_balance_at_this_chain` back to the /// `source_account_at_this_chain`. /// /// This should be called only when transfer has failed at Bridged chain and we have /// received notification about that. #[pallet::weight(T::WeightInfo::cancel_swap())] pub fn cancel_swap( origin: OriginFor, swap: TokenSwapOf, ) -> DispatchResultWithPostInfo { // ensure that the `origin` is the same account that is mentioned in the `swap` // intention let origin_account = ensure_signed(origin)?; ensure!( origin_account == swap.source_account_at_this_chain, Error::::MismatchedSwapSourceOrigin, ); // ensure that the swap has failed let swap_hash = swap.using_encoded(blake2_256).into(); let swap_state = PendingSwaps::::get(swap_hash); match swap_state { Some(TokenSwapState::Started) => fail!(Error::::SwapIsPending), Some(TokenSwapState::Confirmed) => fail!(Error::::SwapIsConfirmed), Some(TokenSwapState::Failed) => { // we allow canceling swap even before lock period is over - the // `source_account_at_this_chain` has already paid for nothing and it is up to // him to decide whether he want to try again }, None => fail!(Error::::SwapIsInactive), } complete_claim::(swap, swap_hash, origin_account, Event::SwapCanceled(swap_hash)) } } #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event, I: 'static = ()> { /// Tokens swap has been started and message has been sent to the bridged message. /// /// The payload is the swap hash and the transfer message nonce. SwapStarted(H256, MessageNonce), /// Token swap has been claimed. SwapClaimed(H256), /// Token swap has been canceled. SwapCanceled(H256), } #[pallet::error] pub enum Error { /// The account that has submitted the `start_claim` doesn't match the /// `TokenSwap::source_account_at_this_chain`. MismatchedSwapSourceOrigin, /// The swap balance in This chain tokens is below existential deposit and can't be made. TooLowBalanceOnThisChain, /// Transfer from This chain account to temporary Swap account has failed. FailedToTransferToSwapAccount, /// Transfer from the temporary Swap account to the derived account of Bridged account has /// failed. FailedToTransferFromSwapAccount, /// The message to transfer tokens on Target chain can't be sent. FailedToSendTransferMessage, /// The same swap is already started. SwapAlreadyStarted, /// Swap outcome is not yet received. SwapIsPending, /// Someone is trying to claim swap that has failed. SwapIsFailed, /// Claiming swap is not allowed. /// /// Now the only possible case when you may get this error, is when you're trying to claim /// swap with `TokenSwapType::LockClaimUntilBlock` before lock period is over. SwapIsTemporaryLocked, /// Swap period is finished and you can not restart it. /// /// Now the only possible case when you may get this error, is when you're trying to start /// swap with `TokenSwapType::LockClaimUntilBlock` after lock period is over. SwapPeriodIsFinished, /// Someone is trying to cancel swap that has been confirmed. SwapIsConfirmed, /// Someone is trying to claim/cancel swap that is either not started or already /// claimed/canceled. SwapIsInactive, /// The swap claimant is invalid. InvalidClaimant, } /// Origin for the token swap pallet. #[pallet::origin] pub type Origin = RawOrigin<::AccountId, I>; /// Pending token swaps states. #[pallet::storage] pub type PendingSwaps, I: 'static = ()> = StorageMap<_, Identity, H256, TokenSwapState>; /// Pending transfer messages. #[pallet::storage] pub type PendingMessages, I: 'static = ()> = StorageMap<_, Identity, MessageNonce, H256>; impl, I: 'static> OnDeliveryConfirmed for Pallet { fn on_messages_delivered(lane: &LaneId, delivered_messages: &DeliveredMessages) -> Weight { // we're only interested in our lane messages if *lane != T::OutboundMessageLaneId::get() { return 0 } // so now we're dealing with our lane messages. Ideally we'll have dedicated lane // and every message from `delivered_messages` is actually our transfer message. // But it may be some shared lane (which is not recommended). let mut reads = 0; let mut writes = 0; for message_nonce in delivered_messages.begin..=delivered_messages.end { reads += 1; if let Some(swap_hash) = PendingMessages::::take(message_nonce) { writes += 1; let token_swap_state = if delivered_messages.message_dispatch_result(message_nonce) { TokenSwapState::Confirmed } else { TokenSwapState::Failed }; log::trace!( target: "runtime::bridge-token-swap", "The dispatch of swap {:?} has been completed with {:?} status", swap_hash, token_swap_state, ); PendingSwaps::::insert(swap_hash, token_swap_state); } } ::DbWeight::get().reads_writes(reads, writes) } } /// Returns temporary account id used to lock funds during swap on This chain. pub(crate) fn swap_account_id, I: 'static>( swap: &TokenSwapOf, ) -> T::AccountId { T::FromSwapToThisAccountIdConverter::convert(swap.using_encoded(blake2_256).into()) } /// Expected target account representation on This chain (aka `target_account_at_this_chain`). pub(crate) fn target_account_at_this_chain, I: 'static>( swap: &TokenSwapOf, ) -> T::AccountId { T::FromBridgedToThisAccountIdConverter::convert(bp_runtime::derive_account_id( T::BridgedChainId::get(), bp_runtime::SourceAccount::Account(swap.target_account_at_bridged_chain.clone()), )) } /// Complete claim with given outcome. pub(crate) fn complete_claim, I: 'static>( swap: TokenSwapOf, swap_hash: H256, destination_account: T::AccountId, event: Event, ) -> DispatchResultWithPostInfo { let swap_account = swap_account_id::(&swap); frame_support::storage::with_transaction( || -> sp_runtime::TransactionOutcome> { // funds are transferred from the temporary Swap account to the destination account let transfer_result = T::ThisCurrency::transfer( &swap_account, &destination_account, swap.source_balance_at_this_chain, ExistenceRequirement::AllowDeath, ); if let Err(err) = transfer_result { log::error!( target: "runtime::bridge-token-swap", "Failed to transfer This chain tokens for the swap {:?} from the Swap account {:?} to {:?}: {:?}", swap, swap_account, destination_account, err, ); return sp_runtime::TransactionOutcome::Rollback(Err( Error::::FailedToTransferFromSwapAccount.into(), )) } log::trace!( target: "runtime::bridge-token-swap", "The swap {:?} (hash {:?}) has been completed with {} status", swap, swap_hash, match event { Event::SwapClaimed(_) => "claimed", Event::SwapCanceled(_) => "canceled", _ => "", }, ); // forget about swap PendingSwaps::::remove(swap_hash); // finally - emit the event Pallet::::deposit_event(event); sp_runtime::TransactionOutcome::Commit(Ok(Ok(().into()))) }, )? } } #[cfg(test)] mod tests { use super::*; use crate::mock::*; use frame_support::{assert_noop, assert_ok, storage::generator::StorageMap}; const CAN_START_BLOCK_NUMBER: u64 = 10; const CAN_CLAIM_BLOCK_NUMBER: u64 = CAN_START_BLOCK_NUMBER + 1; const BRIDGED_CHAIN_ACCOUNT: BridgedAccountId = 3; const BRIDGED_CHAIN_SPEC_VERSION: u32 = 4; const BRIDGED_CHAIN_CALL_WEIGHT: Balance = 5; fn bridged_chain_account_public() -> BridgedAccountPublic { 1.into() } fn bridged_chain_account_signature() -> BridgedAccountSignature { sp_runtime::testing::TestSignature(2, Vec::new()) } fn test_swap() -> TokenSwapOf { bp_token_swap::TokenSwap { swap_type: TokenSwapType::LockClaimUntilBlock(CAN_START_BLOCK_NUMBER, 0.into()), source_balance_at_this_chain: 100, source_account_at_this_chain: THIS_CHAIN_ACCOUNT, target_balance_at_bridged_chain: 200, target_account_at_bridged_chain: BRIDGED_CHAIN_ACCOUNT, } } fn test_swap_creation() -> TokenSwapCreationOf { TokenSwapCreation { target_public_at_bridged_chain: bridged_chain_account_public(), swap_delivery_and_dispatch_fee: SWAP_DELIVERY_AND_DISPATCH_FEE, bridged_chain_spec_version: BRIDGED_CHAIN_SPEC_VERSION, bridged_currency_transfer: test_transfer(), bridged_currency_transfer_weight: BRIDGED_CHAIN_CALL_WEIGHT, bridged_currency_transfer_signature: bridged_chain_account_signature(), } } fn test_swap_hash() -> H256 { test_swap().using_encoded(blake2_256).into() } fn test_transfer() -> RawBridgedTransferCall { vec![OK_TRANSFER_CALL] } fn start_test_swap() { assert_ok!(Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap(), Box::new(TokenSwapCreation { target_public_at_bridged_chain: bridged_chain_account_public(), swap_delivery_and_dispatch_fee: SWAP_DELIVERY_AND_DISPATCH_FEE, bridged_chain_spec_version: BRIDGED_CHAIN_SPEC_VERSION, bridged_currency_transfer: test_transfer(), bridged_currency_transfer_weight: BRIDGED_CHAIN_CALL_WEIGHT, bridged_currency_transfer_signature: bridged_chain_account_signature(), }), )); } fn receive_test_swap_confirmation(success: bool) { Pallet::::on_messages_delivered( &OutboundMessageLaneId::get(), &DeliveredMessages::new(MESSAGE_NONCE, success), ); } #[test] fn create_swap_fails_if_origin_is_incorrect() { run_test(|| { assert_noop!( Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT + 1), test_swap(), Box::new(test_swap_creation()), ), Error::::MismatchedSwapSourceOrigin ); }); } #[test] fn create_swap_fails_if_this_chain_balance_is_below_existential_deposit() { run_test(|| { let mut swap = test_swap(); swap.source_balance_at_this_chain = ExistentialDeposit::get() - 1; assert_noop!( Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), swap, Box::new(test_swap_creation()), ), Error::::TooLowBalanceOnThisChain ); }); } #[test] fn create_swap_fails_if_currency_transfer_to_swap_account_fails() { run_test(|| { let mut swap = test_swap(); swap.source_balance_at_this_chain = THIS_CHAIN_ACCOUNT_BALANCE + 1; assert_noop!( Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), swap, Box::new(test_swap_creation()), ), Error::::FailedToTransferToSwapAccount ); }); } #[test] fn create_swap_fails_if_send_message_fails() { run_test(|| { let mut transfer = test_transfer(); transfer[0] = BAD_TRANSFER_CALL; let mut swap_creation = test_swap_creation(); swap_creation.bridged_currency_transfer = transfer; assert_noop!( Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap(), Box::new(swap_creation), ), Error::::FailedToSendTransferMessage ); }); } #[test] fn create_swap_fails_if_swap_is_active() { run_test(|| { assert_ok!(Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap(), Box::new(test_swap_creation()), )); assert_noop!( Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap(), Box::new(test_swap_creation()), ), Error::::SwapAlreadyStarted ); }); } #[test] fn create_swap_fails_if_trying_to_start_swap_after_lock_period_is_finished() { run_test(|| { frame_system::Pallet::::set_block_number(CAN_START_BLOCK_NUMBER + 1); assert_noop!( Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap(), Box::new(test_swap_creation()), ), Error::::SwapPeriodIsFinished ); }); } #[test] fn create_swap_succeeds_if_trying_to_start_swap_at_lock_period_end() { run_test(|| { frame_system::Pallet::::set_block_number(CAN_START_BLOCK_NUMBER); assert_ok!(Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap(), Box::new(test_swap_creation()), )); }); } #[test] fn create_swap_succeeds() { run_test(|| { frame_system::Pallet::::set_block_number(1); frame_system::Pallet::::reset_events(); assert_ok!(Pallet::::create_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap(), Box::new(test_swap_creation()), )); let swap_hash = test_swap_hash(); assert_eq!(PendingSwaps::::get(swap_hash), Some(TokenSwapState::Started)); assert_eq!(PendingMessages::::get(MESSAGE_NONCE), Some(swap_hash)); assert_eq!( pallet_balances::Pallet::::free_balance(&swap_account_id::< TestRuntime, (), >(&test_swap())), test_swap().source_balance_at_this_chain + SWAP_DELIVERY_AND_DISPATCH_FEE, ); assert!( frame_system::Pallet::::events().iter().any(|e| e.event == crate::mock::Event::TokenSwap(crate::Event::SwapStarted( swap_hash, MESSAGE_NONCE, ))), "Missing SwapStarted event: {:?}", frame_system::Pallet::::events(), ); }); } #[test] fn claim_swap_fails_if_origin_is_incorrect() { run_test(|| { assert_noop!( Pallet::::claim_swap( mock::Origin::signed( 1 + target_account_at_this_chain::(&test_swap()) ), test_swap(), ), Error::::InvalidClaimant ); }); } #[test] fn claim_swap_fails_if_swap_is_pending() { run_test(|| { PendingSwaps::::insert(test_swap_hash(), TokenSwapState::Started); assert_noop!( Pallet::::claim_swap( mock::Origin::signed(target_account_at_this_chain::( &test_swap() )), test_swap(), ), Error::::SwapIsPending ); }); } #[test] fn claim_swap_fails_if_swap_is_failed() { run_test(|| { PendingSwaps::::insert(test_swap_hash(), TokenSwapState::Failed); assert_noop!( Pallet::::claim_swap( mock::Origin::signed(target_account_at_this_chain::( &test_swap() )), test_swap(), ), Error::::SwapIsFailed ); }); } #[test] fn claim_swap_fails_if_swap_is_inactive() { run_test(|| { assert_noop!( Pallet::::claim_swap( mock::Origin::signed(target_account_at_this_chain::( &test_swap() )), test_swap(), ), Error::::SwapIsInactive ); }); } #[test] fn claim_swap_fails_if_currency_transfer_from_swap_account_fails() { run_test(|| { frame_system::Pallet::::set_block_number(CAN_CLAIM_BLOCK_NUMBER); PendingSwaps::::insert(test_swap_hash(), TokenSwapState::Confirmed); assert_noop!( Pallet::::claim_swap( mock::Origin::signed(target_account_at_this_chain::( &test_swap() )), test_swap(), ), Error::::FailedToTransferFromSwapAccount ); }); } #[test] fn claim_swap_fails_before_lock_period_is_completed() { run_test(|| { start_test_swap(); receive_test_swap_confirmation(true); frame_system::Pallet::::set_block_number(CAN_CLAIM_BLOCK_NUMBER - 1); assert_noop!( Pallet::::claim_swap( mock::Origin::signed(target_account_at_this_chain::( &test_swap() )), test_swap(), ), Error::::SwapIsTemporaryLocked ); }); } #[test] fn claim_swap_succeeds() { run_test(|| { start_test_swap(); receive_test_swap_confirmation(true); frame_system::Pallet::::set_block_number(CAN_CLAIM_BLOCK_NUMBER); frame_system::Pallet::::reset_events(); assert_ok!(Pallet::::claim_swap( mock::Origin::signed(target_account_at_this_chain::(&test_swap())), test_swap(), )); let swap_hash = test_swap_hash(); assert_eq!(PendingSwaps::::get(swap_hash), None); assert_eq!( pallet_balances::Pallet::::free_balance(&swap_account_id::< TestRuntime, (), >(&test_swap())), 0, ); assert_eq!( pallet_balances::Pallet::::free_balance( &target_account_at_this_chain::(&test_swap()), ), test_swap().source_balance_at_this_chain, ); assert!( frame_system::Pallet::::events().iter().any(|e| e.event == crate::mock::Event::TokenSwap(crate::Event::SwapClaimed(swap_hash,))), "Missing SwapClaimed event: {:?}", frame_system::Pallet::::events(), ); }); } #[test] fn cancel_swap_fails_if_origin_is_incorrect() { run_test(|| { start_test_swap(); receive_test_swap_confirmation(false); assert_noop!( Pallet::::cancel_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT + 1), test_swap() ), Error::::MismatchedSwapSourceOrigin ); }); } #[test] fn cancel_swap_fails_if_swap_is_pending() { run_test(|| { start_test_swap(); assert_noop!( Pallet::::cancel_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap() ), Error::::SwapIsPending ); }); } #[test] fn cancel_swap_fails_if_swap_is_confirmed() { run_test(|| { start_test_swap(); receive_test_swap_confirmation(true); assert_noop!( Pallet::::cancel_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap() ), Error::::SwapIsConfirmed ); }); } #[test] fn cancel_swap_fails_if_swap_is_inactive() { run_test(|| { assert_noop!( Pallet::::cancel_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap() ), Error::::SwapIsInactive ); }); } #[test] fn cancel_swap_fails_if_currency_transfer_from_swap_account_fails() { run_test(|| { start_test_swap(); receive_test_swap_confirmation(false); let _ = pallet_balances::Pallet::::slash( &swap_account_id::(&test_swap()), test_swap().source_balance_at_this_chain, ); assert_noop!( Pallet::::cancel_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap() ), Error::::FailedToTransferFromSwapAccount ); }); } #[test] fn cancel_swap_succeeds() { run_test(|| { start_test_swap(); receive_test_swap_confirmation(false); frame_system::Pallet::::set_block_number(1); frame_system::Pallet::::reset_events(); assert_ok!(Pallet::::cancel_swap( mock::Origin::signed(THIS_CHAIN_ACCOUNT), test_swap() )); let swap_hash = test_swap_hash(); assert_eq!(PendingSwaps::::get(swap_hash), None); assert_eq!( pallet_balances::Pallet::::free_balance(&swap_account_id::< TestRuntime, (), >(&test_swap())), 0, ); assert_eq!( pallet_balances::Pallet::::free_balance(&THIS_CHAIN_ACCOUNT), THIS_CHAIN_ACCOUNT_BALANCE - SWAP_DELIVERY_AND_DISPATCH_FEE, ); assert!( frame_system::Pallet::::events().iter().any(|e| e.event == crate::mock::Event::TokenSwap(crate::Event::SwapCanceled(swap_hash,))), "Missing SwapCanceled event: {:?}", frame_system::Pallet::::events(), ); }); } #[test] fn messages_delivery_confirmations_are_accepted() { run_test(|| { start_test_swap(); assert_eq!( PendingMessages::::get(MESSAGE_NONCE), Some(test_swap_hash()) ); assert_eq!( PendingSwaps::::get(test_swap_hash()), Some(TokenSwapState::Started) ); // when unrelated messages are delivered let mut messages = DeliveredMessages::new(MESSAGE_NONCE - 2, true); messages.note_dispatched_message(false); Pallet::::on_messages_delivered( &OutboundMessageLaneId::get(), &messages, ); assert_eq!( PendingMessages::::get(MESSAGE_NONCE), Some(test_swap_hash()) ); assert_eq!( PendingSwaps::::get(test_swap_hash()), Some(TokenSwapState::Started) ); // when message we're interested in is accompanied by a bunch of other messages let mut messages = DeliveredMessages::new(MESSAGE_NONCE - 1, false); messages.note_dispatched_message(true); messages.note_dispatched_message(false); Pallet::::on_messages_delivered( &OutboundMessageLaneId::get(), &messages, ); assert_eq!(PendingMessages::::get(MESSAGE_NONCE), None); assert_eq!( PendingSwaps::::get(test_swap_hash()), Some(TokenSwapState::Confirmed) ); }); } #[test] fn storage_keys_computed_properly() { assert_eq!( PendingSwaps::::storage_map_final_key(test_swap_hash()), bp_token_swap::storage_keys::pending_swaps_key("TokenSwap", test_swap_hash()).0, ); } }