diff --git a/Cargo.lock b/Cargo.lock index d7e17b8df6..7ad3d3108a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -851,6 +851,15 @@ dependencies = [ "sp-std", ] +[[package]] +name = "bp-token-swap" +version = "0.1.0" +dependencies = [ + "frame-support", + "parity-scale-codec", + "sp-core", +] + [[package]] name = "bp-westend" version = "0.1.0" @@ -4969,6 +4978,27 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-bridge-token-swap" +version = "0.1.0" +dependencies = [ + "bp-message-dispatch", + "bp-messages", + "bp-runtime", + "bp-token-swap", + "frame-support", + "frame-system", + "log", + "pallet-balances", + "pallet-bridge-dispatch", + "parity-scale-codec", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-grandpa" version = "3.1.0" diff --git a/modules/token-swap/Cargo.toml b/modules/token-swap/Cargo.toml new file mode 100644 index 0000000000..8a83a88503 --- /dev/null +++ b/modules/token-swap/Cargo.toml @@ -0,0 +1,51 @@ +[package] +name = "pallet-bridge-token-swap" +description = "An Substrate pallet that allows parties on different chains (bridged using messages pallet) to swap their tokens" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } +log = { version = "0.4.14", default-features = false } +serde = { version = "1.0", optional = true } + +# Bridge dependencies + +bp-message-dispatch = { path = "../../primitives/message-dispatch", default-features = false } +bp-messages = { path = "../../primitives/messages", default-features = false } +bp-runtime = { path = "../../primitives/runtime", default-features = false } +bp-token-swap = { path = "../../primitives/token-swap", default-features = false } +pallet-bridge-dispatch = { path = "../dispatch", default-features = false } + +# Substrate Dependencies + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false, optional = true } + +[dev-dependencies] +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "master" } + +[features] +default = ["std"] +std = [ + "codec/std", + "bp-message-dispatch/std", + "bp-messages/std", + "bp-runtime/std", + "bp-token-swap/std", + "frame-support/std", + "frame-system/std", + "log/std", + "pallet-bridge-dispatch/std", + "serde", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", +] diff --git a/modules/token-swap/src/lib.rs b/modules/token-swap/src/lib.rs new file mode 100644 index 0000000000..a1306c2505 --- /dev/null +++ b/modules/token-swap/src/lib.rs @@ -0,0 +1,1028 @@ +// 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. + +use bp_messages::{ + source_chain::{MessagesBridge, OnDeliveryConfirmed}, + DeliveredMessages, LaneId, MessageNonce, +}; +use bp_runtime::{messages::DispatchFeePayment, ChainId}; +use bp_token_swap::{TokenSwap, TokenSwapType}; +use codec::{Decode, Encode}; +use frame_support::{ + fail, + traits::{Currency, ExistenceRequirement}, + RuntimeDebug, +}; +use sp_core::H256; +use sp_io::hashing::blake2_256; +use sp_runtime::traits::{Convert, Saturating}; + +#[cfg(test)] +mod mock; + +/// Pending token swap state. +#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq)] +pub enum TokenSwapState { + /// The swap has been started using the `start_claim` call, but we have no proof that it has + /// happened at the Bridged chain. + Started, + /// The swap has happened at the Bridged chain and may be claimed by the Bridged chain party using + /// the `claim_swap` call. + Confirmed, + /// The swap has failed at the Bridged chain and This chain party may cancel it using the + /// `cancel_swap` call. + Failed, +} + +pub use pallet::*; + +// 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>; + + /// Id of the bridge with the Bridged chain. + type BridgeChainId: 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::AccountId, + >::Balance, + MessagePayloadOf, + >; + /// Message delivery and dispatch fee for the tokens transfer message heading to the Bridged chain. + type MessageDeliveryAndDispatchFee: Get<>::Balance>; + + /// 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; + + /// Tokens balance type at the Bridged chain. + type BridgedBalance: Parameter; + /// Account identifier type at the Bridged chain. + type BridgedAccountId: Parameter; + /// Account public key type at the Bridged chain. + type BridgedAccountPublic: Parameter; + /// Account signature type at the Bridged chain. + type BridgedAccountSignature: Parameter; + /// Converter from raw hash (derived from Bridged chain account) to This chain account. + type FromBridgedToThisAccountIdConverter: Convert; + } + + /// SCALE-encoded `Currency::transfer` call on the bridged chain. + pub type RawBridgedTransferCall = Vec; + /// Bridge message payload used by the pallet. + pub type MessagePayloadOf = bp_message_dispatch::MessagePayload< + ::AccountId, + >::BridgedAccountPublic, + >::BridgedAccountSignature, + RawBridgedTransferCall, + >; + /// Type of `TokenSwap` used by the pallet. + pub type TokenSwapOf = TokenSwap< + BlockNumberFor, + <>::ThisCurrency as Currency<::AccountId>>::Balance, + ::AccountId, + >::BridgedBalance, + >::BridgedAccountId, + >; + + #[pallet::pallet] + #[pallet::generate_store(pub(super) trait Store)] + pub struct Pallet(PhantomData<(T, I)>); + + #[pallet::hooks] + impl, I: 'static> Hooks> for Pallet {} + + #[pallet::call] + impl, I: 'static> Pallet { + /// 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; + /// - `target_public_at_bridged_chain` - the public key of the `swap.target_account_at_bridged_chain` + /// account used to verify `bridged_currency_transfer_signature`; + /// - `bridged_currency_transfer` - the SCALE-encoded tokens transfer call at the Bridged chain; + /// - `bridged_currency_transfer_signature` - the signature of the `swap.target_account_at_bridged_chain` + /// for the message returned by the `pallet_bridge_dispatch::account_ownership_digest()` function call. + /// + /// 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 `bridged_currency_transfer` represents a valid token transfer call that transfers + /// `swap.target_balance_at_bridged_chain` to his `source_account_at_bridged_chain` account; + /// 2) that either the `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 `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 + /// `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. + #[pallet::weight(0)] + pub fn create_swap( + origin: OriginFor, + swap: TokenSwapOf, + target_public_at_bridged_chain: T::BridgedAccountPublic, + bridged_chain_spec_version: u32, + bridged_currency_transfer: RawBridgedTransferCall, + bridged_currency_transfer_weight: Weight, + bridged_currency_transfer_signature: T::BridgedAccountSignature, + ) -> 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, + ); + + // 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); + frame_support::storage::with_transaction(|| { + // funds are transferred from This account to the temporary Swap account + let message_delivery_and_dispatch_fee = T::MessageDeliveryAndDispatchFee::get(); + 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(message_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( + swap_account.clone(), + 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, + }, + message_delivery_and_dispatch_fee, + ); + let transfer_message_nonce = match send_message_result { + Ok(transfer_message_nonce) => transfer_message_nonce, + 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())); + } + + // remember that we're waiting for the transfer message delivery confirmation + PendingMessages::::insert(transfer_message_nonce, swap_hash); + + // finally - emit the event + Self::deposit_event(Event::SwapStarted(swap_hash, transfer_message_nonce)); + + sp_runtime::TransactionOutcome::Commit(Ok(().into())) + }) + } + + /// 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(0)] + 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 thate. + #[pallet::weight(0)] + 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 cancelling 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::SwapCancelled(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 cancelled. + SwapCancelled(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/cancelled. + SwapIsInactive, + /// The swap claimant is invalid. + InvalidClaimant, + } + + /// 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; + PendingSwaps::::insert( + swap_hash, + if delivered_messages.message_dispatch_result(message_nonce) { + TokenSwapState::Confirmed + } else { + TokenSwapState::Failed + }, + ); + } + } + + ::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::BridgeChainId::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(|| { + // 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() + )); + } + + // forget about swap + PendingSwaps::::remove(swap_hash); + + // finally - emit the event + Pallet::::deposit_event(event); + + sp_runtime::TransactionOutcome::Commit(Ok(().into())) + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::mock::*; + use frame_support::{assert_noop, assert_ok}; + + const CAN_START_BLOCK_NUMBER: u64 = 10; + const CAN_CLAIM_BLOCK_NUMBER: u64 = CAN_START_BLOCK_NUMBER + 1; + + const BRIDGED_CHAIN_ACCOUNT_PUBLIC: BridgedAccountPublic = 1; + const BRIDGED_CHAIN_ACCOUNT_SIGNATURE: BridgedAccountSignature = 2; + const BRIDGED_CHAIN_ACCOUNT: BridgedAccountId = 3; + const BRIDGED_CHAIN_SPEC_VERSION: u32 = 4; + const BRIDGED_CHAIN_CALL_WEIGHT: Balance = 5; + + 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_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( + Origin::signed(THIS_CHAIN_ACCOUNT), + test_swap(), + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + 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( + Origin::signed(THIS_CHAIN_ACCOUNT + 1), + test_swap(), + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + ), + 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( + Origin::signed(THIS_CHAIN_ACCOUNT), + swap, + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + ), + 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( + Origin::signed(THIS_CHAIN_ACCOUNT), + swap, + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + ), + Error::::FailedToTransferToSwapAccount + ); + }); + } + + #[test] + fn create_swap_fails_if_send_message_fails() { + run_test(|| { + let mut transfer = test_transfer(); + transfer[0] = BAD_TRANSFER_CALL; + assert_noop!( + Pallet::::create_swap( + Origin::signed(THIS_CHAIN_ACCOUNT), + test_swap(), + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + transfer, + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + ), + Error::::FailedToSendTransferMessage + ); + }); + } + + #[test] + fn create_swap_fails_if_swap_is_active() { + run_test(|| { + assert_ok!(Pallet::::create_swap( + Origin::signed(THIS_CHAIN_ACCOUNT), + test_swap(), + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + )); + + assert_noop!( + Pallet::::create_swap( + Origin::signed(THIS_CHAIN_ACCOUNT), + test_swap(), + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + ), + 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( + Origin::signed(THIS_CHAIN_ACCOUNT), + test_swap(), + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + ), + 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( + Origin::signed(THIS_CHAIN_ACCOUNT), + test_swap(), + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + )); + }); + } + + #[test] + fn create_swap_succeeds() { + run_test(|| { + frame_system::Pallet::::set_block_number(1); + frame_system::Pallet::::reset_events(); + + assert_ok!(Pallet::::create_swap( + Origin::signed(THIS_CHAIN_ACCOUNT), + test_swap(), + BRIDGED_CHAIN_ACCOUNT_PUBLIC, + BRIDGED_CHAIN_SPEC_VERSION, + test_transfer(), + BRIDGED_CHAIN_CALL_WEIGHT, + BRIDGED_CHAIN_ACCOUNT_SIGNATURE, + )); + + 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::(&test_swap())), + test_swap().source_balance_at_this_chain + MessageDeliveryAndDispatchFee::get(), + ); + 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( + 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( + 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( + 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( + 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( + 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( + 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( + 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::(&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(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(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(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(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(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( + 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::(&test_swap())), + 0, + ); + assert_eq!( + pallet_balances::Pallet::::free_balance(&THIS_CHAIN_ACCOUNT), + THIS_CHAIN_ACCOUNT_BALANCE - MessageDeliveryAndDispatchFee::get(), + ); + assert!( + frame_system::Pallet::::events() + .iter() + .any(|e| e.event == crate::mock::Event::TokenSwap(crate::Event::SwapCancelled(swap_hash,))), + "Missing SwapCancelled 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) + ); + }); + } +} diff --git a/modules/token-swap/src/mock.rs b/modules/token-swap/src/mock.rs new file mode 100644 index 0000000000..c91933cce4 --- /dev/null +++ b/modules/token-swap/src/mock.rs @@ -0,0 +1,174 @@ +// 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 . + +use crate as pallet_bridge_token_swap; +use crate::MessagePayloadOf; + +use bp_messages::{source_chain::MessagesBridge, LaneId, MessageNonce}; +use bp_runtime::ChainId; +use frame_support::weights::Weight; +use sp_core::H256; +use sp_runtime::{ + testing::Header as SubstrateHeader, + traits::{BlakeTwo256, IdentityLookup}, + Perbill, +}; + +pub type AccountId = u64; +pub type Balance = u64; +pub type Block = frame_system::mocking::MockBlock; +pub type BridgedAccountId = u64; +pub type BridgedAccountPublic = u64; +pub type BridgedAccountSignature = u64; +pub type BridgedBalance = u64; +pub type UncheckedExtrinsic = frame_system::mocking::MockUncheckedExtrinsic; + +pub const OK_TRANSFER_CALL: u8 = 1; +pub const BAD_TRANSFER_CALL: u8 = 2; +pub const MESSAGE_NONCE: MessageNonce = 3; + +pub const THIS_CHAIN_ACCOUNT: AccountId = 1; +pub const THIS_CHAIN_ACCOUNT_BALANCE: Balance = 100_000; + +frame_support::construct_runtime! { + pub enum TestRuntime where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system::{Pallet, Call, Config, Storage, Event}, + Balances: pallet_balances::{Pallet, Call, Event}, + TokenSwap: pallet_bridge_token_swap::{Pallet, Call, Event}, + } +} + +frame_support::parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const MaximumBlockWeight: Weight = 1024; + pub const MaximumBlockLength: u32 = 2 * 1024; + pub const AvailableBlockRatio: Perbill = Perbill::one(); +} + +impl frame_system::Config for TestRuntime { + type Origin = Origin; + type Index = u64; + type Call = Call; + type BlockNumber = u64; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = SubstrateHeader; + type Event = Event; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type BaseCallFilter = (); + type SystemWeightInfo = (); + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type SS58Prefix = (); + type OnSetCode = (); +} + +frame_support::parameter_types! { + pub const ExistentialDeposit: u64 = 10; + pub const MaxReserves: u32 = 50; +} + +impl pallet_balances::Config for TestRuntime { + type MaxLocks = (); + type Balance = Balance; + type DustRemoval = (); + type Event = Event; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = frame_system::Pallet; + type WeightInfo = (); + type MaxReserves = MaxReserves; + type ReserveIdentifier = [u8; 8]; +} + +frame_support::parameter_types! { + pub const BridgeChainId: ChainId = *b"inst"; + pub const OutboundMessageLaneId: LaneId = *b"lane"; + pub const MessageDeliveryAndDispatchFee: Balance = 1; +} + +impl pallet_bridge_token_swap::Config for TestRuntime { + type Event = Event; + + type BridgeChainId = BridgeChainId; + type OutboundMessageLaneId = OutboundMessageLaneId; + type MessagesBridge = TestMessagesBridge; + type MessageDeliveryAndDispatchFee = MessageDeliveryAndDispatchFee; + + type ThisCurrency = pallet_balances::Pallet; + type FromSwapToThisAccountIdConverter = TestAccountConverter; + + type BridgedBalance = BridgedBalance; + type BridgedAccountId = BridgedAccountId; + type BridgedAccountPublic = BridgedAccountPublic; + type BridgedAccountSignature = BridgedAccountSignature; + type FromBridgedToThisAccountIdConverter = TestAccountConverter; +} + +pub struct TestMessagesBridge; + +impl MessagesBridge> for TestMessagesBridge { + type Error = (); + + fn send_message( + sender: AccountId, + lane: LaneId, + message: MessagePayloadOf, + delivery_and_dispatch_fee: Balance, + ) -> Result { + assert_ne!(sender, THIS_CHAIN_ACCOUNT); + assert_eq!(lane, OutboundMessageLaneId::get()); + assert_eq!(delivery_and_dispatch_fee, MessageDeliveryAndDispatchFee::get()); + match message.call[0] { + OK_TRANSFER_CALL => Ok(MESSAGE_NONCE), + BAD_TRANSFER_CALL => Err(()), + _ => unreachable!(), + } + } +} + +pub struct TestAccountConverter; + +impl sp_runtime::traits::Convert for TestAccountConverter { + fn convert(hash: H256) -> AccountId { + hash.to_low_u64_ne() + } +} + +/// Run pallet test. +pub fn run_test(test: impl FnOnce() -> T) -> T { + let mut t = frame_system::GenesisConfig::default() + .build_storage::() + .unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(THIS_CHAIN_ACCOUNT, THIS_CHAIN_ACCOUNT_BALANCE)], + } + .assimilate_storage(&mut t) + .unwrap(); + let mut ext = sp_io::TestExternalities::new(t); + ext.execute_with(test) +} diff --git a/primitives/messages/src/source_chain.rs b/primitives/messages/src/source_chain.rs index ed69f39c76..a87d03ca9d 100644 --- a/primitives/messages/src/source_chain.rs +++ b/primitives/messages/src/source_chain.rs @@ -135,6 +135,22 @@ pub trait MessageDeliveryAndDispatchPayment { } } +/// Messages bridge API to be used from other pallets. +pub trait MessagesBridge { + /// Error type. + type Error: Debug; + + /// Send message over the bridge. + /// + /// Returns unique message nonce or error if send has failed. + fn send_message( + sender: AccountId, + lane: LaneId, + message: Payload, + delivery_and_dispatch_fee: Balance, + ) -> Result; +} + /// Handler for messages delivery confirmation. pub trait OnDeliveryConfirmed { /// Called when we receive confirmation that our messages have been delivered to the diff --git a/primitives/token-swap/Cargo.toml b/primitives/token-swap/Cargo.toml new file mode 100644 index 0000000000..4245a5363f --- /dev/null +++ b/primitives/token-swap/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "bp-token-swap" +description = "Primitives of the pallet-bridge-token-swap pallet" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2018" +license = "GPL-3.0-or-later WITH Classpath-exception-2.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "2.0.0", default-features = false } + +# Substrate Dependencies + +frame-support = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "master" , default-features = false } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-support/std", + "sp-core/std", +] diff --git a/primitives/token-swap/src/lib.rs b/primitives/token-swap/src/lib.rs new file mode 100644 index 0000000000..e5575e73b0 --- /dev/null +++ b/primitives/token-swap/src/lib.rs @@ -0,0 +1,64 @@ +// 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 . + +use codec::{Decode, Encode}; +use frame_support::RuntimeDebug; +use sp_core::U256; + +/// Token swap type. +/// +/// Different swap types give a different guarantees regarding possible swap +/// replay protection. +#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq)] +pub enum TokenSwapType { + /// The `target_account_at_bridged_chain` is temporary and only have funds for single swap. + /// + /// ***WARNING**: if `target_account_at_bridged_chain` still exists after the swap has been + /// completed (either by claiming or cancelling), the `source_account_at_this_chain` will be able + /// to restart the swap again and repeat the swap until `target_account_at_bridged_chain` depletes. + TemporaryTargetAccountAtBridgedChain, + /// This swap type prevents `source_account_at_this_chain` from restarting the swap after it has + /// been completed. There are two consequences: + /// + /// 1) the `source_account_at_this_chain` won't be able to call `start_swap` after given ; + /// 2) the `target_account_at_bridged_chain` won't be able to call `claim_swap` (over the bridge) before + /// block ``. + /// + /// The second element is the nonce of the swap. You must care about its uniqueness if you're + /// planning to perform another swap with exactly the same parameters (i.e. same amount, same accounts, + /// same `ThisBlockNumber`) to avoid collisions. + LockClaimUntilBlock(ThisBlockNumber, U256), +} + +/// An intention to swap `source_balance_at_this_chain` owned by `source_account_at_this_chain` +/// to `target_balance_at_bridged_chain` owned by `target_account_at_bridged_chain`. +/// +/// **IMPORTANT NOTE**: this structure is always the same during single token swap. So even +/// when chain changes, the meaning of This and Bridged are still used to point to the same chains. +/// This chain is always the chain where swap has been started. And the Bridged chain is the other chain. +#[derive(Encode, Decode, Clone, RuntimeDebug, PartialEq, Eq)] +pub struct TokenSwap { + /// The type of the swap. + pub swap_type: TokenSwapType, + /// This chain balance to be swapped with `target_balance_at_bridged_chain`. + pub source_balance_at_this_chain: ThisBalance, + /// Account id of the party acting at This chain and owning the `source_account_at_this_chain`. + pub source_account_at_this_chain: ThisAccountId, + /// Bridged chain balance to be swapped with `source_balance_at_this_chain`. + pub target_balance_at_bridged_chain: BridgedBalance, + /// Account id of the party acting at the Bridged chain and owning the `target_balance_at_bridged_chain`. + pub target_account_at_bridged_chain: BridgedAccountId, +}