Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 55 additions & 24 deletions crates/pallet-currency-swap/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,33 +108,64 @@ pub mod pallet {
let who = ensure_signed(origin)?;

with_storage_layer(move || {
let withdrawed_imbalance = FromCurrencyOf::<T>::withdraw(
&who,
amount,
WithdrawReasons::TRANSFER,
ExistenceRequirement::AllowDeath,
)?;
let withdrawed_amount = withdrawed_imbalance.peek();

let deposited_imbalance =
T::CurrencySwap::swap(withdrawed_imbalance).map_err(|error| {
// Here we undo the withdrawl to avoid having a dangling imbalance.
FromCurrencyOf::<T>::resolve_creating(&who, error.incoming_imbalance);
error.cause.into()
})?;
let deposited_amount = deposited_imbalance.peek();

ToCurrencyOf::<T>::resolve_creating(&to, deposited_imbalance);

Self::deposit_event(Event::BalancesSwapped {
from: who,
withdrawed_amount,
to,
deposited_amount,
});
Self::do_swap(who, to, amount, ExistenceRequirement::AllowDeath)?;

Ok(())
})
}

/// Same as the swap call, but with a check that the swap will not kill the origin account.
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::swap_keep_alive())]
pub fn swap_keep_alive(
origin: OriginFor<T>,
to: T::AccountIdTo,
amount: FromBalanceOf<T>,
) -> DispatchResult {
let who = ensure_signed(origin)?;

with_storage_layer(move || {
Self::do_swap(who, to, amount, ExistenceRequirement::KeepAlive)?;

Ok(())
})
}
}

impl<T: Config> Pallet<T> {
/// General swap balances implementation.
pub fn do_swap(
who: T::AccountId,
to: T::AccountIdTo,
amount: FromBalanceOf<T>,
existence_requirement: ExistenceRequirement,
) -> DispatchResult {
let withdrawed_imbalance = FromCurrencyOf::<T>::withdraw(
&who,
amount,
WithdrawReasons::TRANSFER,
existence_requirement,
)?;
let withdrawed_amount = withdrawed_imbalance.peek();

let deposited_imbalance =
T::CurrencySwap::swap(withdrawed_imbalance).map_err(|error| {
// Here we undo the withdrawl to avoid having a dangling imbalance.
FromCurrencyOf::<T>::resolve_creating(&who, error.incoming_imbalance);
error.cause.into()
})?;
let deposited_amount = deposited_imbalance.peek();

ToCurrencyOf::<T>::resolve_creating(&to, deposited_imbalance);

Self::deposit_event(Event::BalancesSwapped {
from: who,
withdrawed_amount,
to,
deposited_amount,
});

Ok(())
}
}
}
4 changes: 2 additions & 2 deletions crates/pallet-currency-swap/src/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ impl pallet_balances::Config for Test {
type Balance = u64;
type RuntimeEvent = RuntimeEvent;
type DustRemoval = ();
type ExistentialDeposit = ConstU64<1>;
type ExistentialDeposit = ConstU64<10>;
type AccountStore = System;
type MaxLocks = ();
type MaxReserves = ();
Expand All @@ -90,7 +90,7 @@ impl pallet_evm_balances::Config for Test {
type RuntimeEvent = RuntimeEvent;
type AccountId = EvmAccountId;
type Balance = Balance;
type ExistentialDeposit = ConstU64<1>;
type ExistentialDeposit = ConstU64<10>;
type AccountStore = EvmSystem;
type DustRemoval = ();
}
Expand Down
145 changes: 144 additions & 1 deletion crates/pallet-currency-swap/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ use sp_std::str::FromStr;

use crate::{mock::*, *};

/// This test verifies that swap call works in the happy path.
/// This test verifies that swap call works as expected in case origin left balances amount
/// is greater or equal than existential deposit.
#[test]
fn swap_works() {
new_test_ext().execute_with_ext(|_| {
Expand Down Expand Up @@ -64,6 +65,116 @@ fn swap_works() {
});
}

/// This test verifies that swap call works as expected in case origin left balances amount
/// is less than existential deposit. The origin account should be killed.
#[test]
fn swap_works_kill_origin() {
new_test_ext().execute_with_ext(|_| {
let alice = 42;
let alice_evm = H160::from_str("1000000000000000000000000000000000000001").unwrap();
let alice_balance = 1000;
let swap_balance = 999;

// Prepare the test state.
Balances::make_free_balance_be(&alice, alice_balance);

// Check test preconditions.
assert_eq!(Balances::total_balance(&alice), alice_balance);
assert_eq!(EvmBalances::total_balance(&alice_evm), 0);

// Set block number to enable events.
System::set_block_number(1);

// Set mock expectations.
let swap_ctx = MockCurrencySwap::swap_context();
swap_ctx
.expect()
.once()
.with(predicate::eq(
<Balances as Currency<AccountId>>::NegativeImbalance::new(swap_balance),
))
.return_once(move |_| {
Ok(<EvmBalances as Currency<EvmAccountId>>::NegativeImbalance::new(swap_balance))
});

// Invoke the function under test.
assert_ok!(CurrencySwap::swap(
RuntimeOrigin::signed(alice),
alice_evm,
swap_balance
));

// Assert state changes.
assert!(!System::account_exists(&alice));
assert_eq!(EvmBalances::total_balance(&alice_evm), swap_balance);
System::assert_has_event(RuntimeEvent::CurrencySwap(Event::BalancesSwapped {
from: alice,
withdrawed_amount: swap_balance,
to: alice_evm,
deposited_amount: swap_balance,
}));

// Assert mock invocations.
swap_ctx.checkpoint();
});
}

/// This test verifies that `swap_keep_alive` call works in the happy path.
#[test]
fn swap_keep_alive_works() {
new_test_ext().execute_with_ext(|_| {
let alice = 42;
let alice_evm = H160::from_str("1000000000000000000000000000000000000001").unwrap();
let alice_balance = 1000;
let swap_balance = 100;

// Prepare the test state.
Balances::make_free_balance_be(&alice, alice_balance);

// Check test preconditions.
assert_eq!(Balances::total_balance(&alice), alice_balance);
assert_eq!(EvmBalances::total_balance(&alice_evm), 0);

// Set block number to enable events.
System::set_block_number(1);

// Set mock expectations.
let swap_ctx = MockCurrencySwap::swap_context();
swap_ctx
.expect()
.once()
.with(predicate::eq(
<Balances as Currency<AccountId>>::NegativeImbalance::new(swap_balance),
))
.return_once(move |_| {
Ok(<EvmBalances as Currency<EvmAccountId>>::NegativeImbalance::new(swap_balance))
});

// Invoke the function under test.
assert_ok!(CurrencySwap::swap(
RuntimeOrigin::signed(alice),
alice_evm,
swap_balance
));

// Assert state changes.
assert_eq!(
Balances::total_balance(&alice),
alice_balance - swap_balance
);
assert_eq!(EvmBalances::total_balance(&alice_evm), swap_balance);
System::assert_has_event(RuntimeEvent::CurrencySwap(Event::BalancesSwapped {
from: alice,
withdrawed_amount: swap_balance,
to: alice_evm,
deposited_amount: swap_balance,
}));

// Assert mock invocations.
swap_ctx.checkpoint();
});
}

/// This test verifies that swap call fails in case some error happens during the actual swap logic.
#[test]
fn swap_fails() {
Expand Down Expand Up @@ -105,3 +216,35 @@ fn swap_fails() {
swap_ctx.checkpoint();
});
}

/// This test verifies that `swap_keep_alive` call fails in case origin left balances amount
/// is less than existential deposit. The call should prevent swap operation.
#[test]
fn swap_keep_alive_fails() {
new_test_ext().execute_with_ext(|_| {
let alice = 42;
let alice_evm = H160::from_str("1000000000000000000000000000000000000001").unwrap();
let alice_balance = 1000;
let swap_balance = 999;

// Prepare the test state.
Balances::make_free_balance_be(&alice, alice_balance);

// Set mock expectations.
let swap_ctx = MockCurrencySwap::swap_context();
swap_ctx.expect().never();

// Invoke the function under test.
assert_noop!(
CurrencySwap::swap_keep_alive(RuntimeOrigin::signed(alice), alice_evm, swap_balance),
pallet_balances::Error::<Test>::KeepAlive
);

// Assert state changes.
assert_eq!(Balances::total_balance(&alice), alice_balance);
assert_eq!(EvmBalances::total_balance(&alice_evm), 0);

// Assert mock invocations.
swap_ctx.checkpoint();
});
}
7 changes: 7 additions & 0 deletions crates/pallet-currency-swap/src/weights.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,17 @@ use frame_support::weights::Weight;
pub trait WeightInfo {
/// A function to calculate required weights for swap call.
fn swap() -> Weight;

/// A function to calculate required weights for `swap_keep_alive` call.
fn swap_keep_alive() -> Weight;
}

impl WeightInfo for () {
fn swap() -> Weight {
Weight::zero()
}

fn swap_keep_alive() -> Weight {
Weight::zero()
}
}