Skip to content
Merged
Prev Previous commit
Next Next commit
XORless transfer (#758)
* XORless transfer

* Add more tests
  • Loading branch information
vovac12 authored Oct 9, 2023
commit 9d1c709eb98971f88f8fb7fb8a196389016f4db1
43 changes: 43 additions & 0 deletions pallets/liquidity-proxy/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2441,6 +2441,49 @@ pub mod pallet {
ADARCommissionRatio::<T>::put(commission_ratio);
Ok(().into())
}

/// Extrinsic which is enable XORless transfers.
/// Internally it's swaps `asset_id` to `desired_xor_amount` of `XOR` and transfers remaining amount of `asset_id` to `receiver`.
/// Client apps should specify the XOR amount which should be paid as a fee in `desired_xor_amount` parameter.
/// If sender will not have enough XOR to pay fees after execution, transaction will be rejected.
/// This extrinsic is done as temporary solution for XORless transfers, in future it would be removed
/// and logic for XORless extrinsics should be moved to xor-fee pallet.
#[pallet::call_index(6)]
#[pallet::weight(Pallet::<T>::swap_weight(dex_id, asset_id, &common::XOR.into(), SwapVariant::WithDesiredOutput).saturating_add(<T as assets::Config>::WeightInfo::transfer()))]
pub fn xorless_transfer(
origin: OriginFor<T>,
dex_id: T::DEXId,
asset_id: T::AssetId,
receiver: T::AccountId,
amount: Balance,
desired_xor_amount: Balance,
max_amount_in: Balance,
selected_source_types: Vec<LiquiditySourceType>,
filter_mode: FilterMode,
) -> DispatchResultWithPostInfo {
let sender = ensure_signed(origin)?;
let mut weight = Weight::default();
if max_amount_in > Balance::zero() && desired_xor_amount > Balance::zero() {
weight = weight.saturating_add(Self::inner_swap(
sender.clone(),
sender.clone(),
dex_id,
asset_id,
common::XOR.into(),
SwapAmount::with_desired_output(desired_xor_amount, max_amount_in),
selected_source_types,
filter_mode,
)?);
}

assets::Pallet::<T>::transfer_from(&asset_id, &sender, &receiver, amount)?;
weight = weight.saturating_add(<T as assets::Config>::WeightInfo::transfer());

Ok(PostDispatchInfo {
actual_weight: Some(weight),
pays_fee: Pays::Yes,
})
}
}

#[pallet::event]
Expand Down
154 changes: 154 additions & 0 deletions pallets/liquidity-proxy/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3763,3 +3763,157 @@ fn test_batch_swap_asset_reuse_fails() {
);
});
}

#[test]
fn test_xorless_transfer_works() {
let mut ext = ExtBuilder::default().with_xyk_pool().build();
ext.execute_with(|| {
assert_eq!(Assets::free_balance(&USDT, &bob()).unwrap(), balance!(0));
assert_eq!(
Assets::free_balance(&USDT, &alice()).unwrap(),
balance!(12000)
);
assert_eq!(
Assets::free_balance(&XOR, &alice()).unwrap(),
balance!(356400)
);

let filter_mode = FilterMode::AllowSelected;
let sources = [LiquiditySourceType::XYKPool].to_vec();

assert_ok!(LiquidityProxy::xorless_transfer(
RuntimeOrigin::signed(alice()),
0,
USDT,
bob(),
balance!(1),
balance!(1),
balance!(10),
sources,
filter_mode,
));

assert_approx_eq!(
Assets::free_balance(&USDT, &alice()).unwrap(),
// 12000 USDT - 1 USDT for swap - 1 USDT for transfer
balance!(11998),
balance!(0.01)
);
assert_approx_eq!(
Assets::free_balance(&XOR, &alice()).unwrap(),
balance!(356401),
balance!(0.01)
);
assert_approx_eq!(
Assets::free_balance(&USDT, &bob()).unwrap(),
balance!(1),
balance!(0.01)
);
});
}

#[test]
fn test_xorless_transfer_without_swap_works() {
let mut ext = ExtBuilder::default().with_xyk_pool().build();
ext.execute_with(|| {
assert_eq!(Assets::free_balance(&USDT, &bob()).unwrap(), balance!(0));
assert_eq!(
Assets::free_balance(&USDT, &alice()).unwrap(),
balance!(12000)
);
assert_eq!(
Assets::free_balance(&XOR, &alice()).unwrap(),
balance!(356400)
);

let filter_mode = FilterMode::AllowSelected;
let sources = [LiquiditySourceType::XYKPool].to_vec();

assert_ok!(LiquidityProxy::xorless_transfer(
RuntimeOrigin::signed(alice()),
0,
USDT,
bob(),
balance!(1),
balance!(0),
balance!(0),
sources,
filter_mode,
));

assert_approx_eq!(
Assets::free_balance(&USDT, &alice()).unwrap(),
// 12000 USDT - 1 USDT for swap - 1 USDT for transfer
balance!(11999),
balance!(0.01)
);
assert_approx_eq!(
Assets::free_balance(&XOR, &alice()).unwrap(),
balance!(356400),
balance!(0.01)
);
assert_approx_eq!(
Assets::free_balance(&USDT, &bob()).unwrap(),
balance!(1),
balance!(0.01)
);
});
}

#[test]
fn test_xorless_transfer_fails_on_swap() {
let mut ext = ExtBuilder::default().with_xyk_pool().build();
ext.execute_with(|| {
assert_eq!(
Assets::free_balance(&USDT, &alice()).unwrap(),
balance!(12000)
);

let filter_mode = FilterMode::AllowSelected;
let sources = [LiquiditySourceType::XYKPool].to_vec();

assert_noop!(
LiquidityProxy::xorless_transfer(
RuntimeOrigin::signed(alice()),
0,
USDT,
bob(),
balance!(1),
balance!(1),
balance!(0.5),
sources,
filter_mode,
),
Error::<Runtime>::SlippageNotTolerated
);
});
}

#[test]
fn test_xorless_transfer_fails_on_transfer() {
let mut ext = ExtBuilder::default().with_xyk_pool().build();
ext.execute_with(|| {
assert_eq!(
Assets::free_balance(&USDT, &alice()).unwrap(),
balance!(12000)
);

let filter_mode = FilterMode::AllowSelected;
let sources = [LiquiditySourceType::XYKPool].to_vec();

assert_noop!(
LiquidityProxy::xorless_transfer(
RuntimeOrigin::signed(alice()),
0,
USDT,
bob(),
balance!(12000),
balance!(1),
balance!(2),
sources,
filter_mode,
),
tokens::Error::<Runtime>::BalanceTooLow
);
});
}
170 changes: 168 additions & 2 deletions runtime/src/tests/xor_fee.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,19 @@ use common::mock::{alice, bob, charlie};
use common::prelude::constants::{BIG_FEE, SMALL_FEE};
use common::prelude::{AssetName, AssetSymbol, FixedWrapper, SwapAmount};
use common::{balance, fixed_wrapper, AssetInfoProvider, FilterMode, VAL, XOR};
use frame_support::assert_ok;
use frame_support::dispatch::{DispatchInfo, PostDispatchInfo};
use frame_support::pallet_prelude::{InvalidTransaction, Pays};
use frame_support::traits::{OnFinalize, OnInitialize};
use frame_support::unsigned::TransactionValidityError;
use frame_support::weights::WeightToFee as WeightToFeeTrait;
use frame_support::{assert_err, assert_ok};
use frame_system::EventRecord;
use framenode_chain_spec::ext;
use log::LevelFilter;
use pallet_balances::NegativeImbalance;
use pallet_transaction_payment::OnChargeTransaction;
use referrals::ReferrerBalances;
use sp_runtime::traits::SignedExtension;
use sp_runtime::traits::{Dispatchable, SignedExtension};
use sp_runtime::{AccountId32, FixedPointNumber, FixedU128};
use traits::MultiCurrency;
use xor_fee::extension::ChargeTransactionPayment;
Expand Down Expand Up @@ -1142,3 +1142,169 @@ fn withdraw_fee_place_limit_order_with_crossing_spread() {
);
});
}

/// Fee should be postponed until after the transaction
#[test]
fn fee_payment_postponed_xorless_transfer() {
ext().execute_with(|| {
set_weight_to_fee_multiplier(1);
increase_balance(alice(), VAL.into(), balance!(1000));

increase_balance(bob(), XOR.into(), balance!(1000));
increase_balance(bob(), VAL.into(), balance!(1000));

ensure_pool_initialized(XOR.into(), VAL.into());
PoolXYK::deposit_liquidity(
RuntimeOrigin::signed(bob()),
0,
XOR.into(),
VAL.into(),
balance!(500),
balance!(500),
balance!(450),
balance!(450),
)
.unwrap();

fill_spot_price();

let dispatch_info = info_from_weight(Weight::from_parts(100_000_000, 0));

let call = RuntimeCall::LiquidityProxy(liquidity_proxy::Call::xorless_transfer {
dex_id: 0,
asset_id: VAL,
// Swap with desired output may return less tokens than requested
desired_xor_amount: 0,
max_amount_in: 0,
amount: balance!(500),
selected_source_types: vec![],
filter_mode: FilterMode::Disabled,
receiver: alice(),
});

let quoted_fee =
xor_fee::Pallet::<Runtime>::withdraw_fee(&bob(), &call, &dispatch_info, SMALL_FEE, 0)
.unwrap();

assert_eq!(
quoted_fee,
LiquidityInfo::Paid(bob(), Some(NegativeImbalance::new(SMALL_FEE)))
);

let call = RuntimeCall::LiquidityProxy(liquidity_proxy::Call::xorless_transfer {
dex_id: 0,
asset_id: VAL,
// Swap with desired output may return less tokens than requested
desired_xor_amount: SMALL_FEE + 1,
max_amount_in: balance!(1),
amount: balance!(10),
selected_source_types: vec![],
filter_mode: FilterMode::Disabled,
receiver: bob(),
});

let quoted_fee =
xor_fee::Pallet::<Runtime>::withdraw_fee(&alice(), &call, &dispatch_info, SMALL_FEE, 0)
.unwrap();

assert_eq!(quoted_fee, LiquidityInfo::Postponed(alice()));

assert_eq!(
Assets::total_balance(&XOR.into(), &alice()).unwrap(),
balance!(0)
);
assert_eq!(
Assets::total_balance(&VAL.into(), &alice()).unwrap(),
balance!(1000)
);

let post_info = call.dispatch(RuntimeOrigin::signed(alice())).unwrap();

assert_eq!(
Assets::total_balance(&XOR.into(), &alice()).unwrap(),
SMALL_FEE
);
assert_eq!(
Assets::total_balance(&VAL.into(), &alice()).unwrap(),
balance!(989.999297892695135178)
);
assert_eq!(
Assets::total_balance(&VAL.into(), &bob()).unwrap(),
balance!(510)
);

assert_ok!(xor_fee::Pallet::<Runtime>::correct_and_deposit_fee(
&alice(),
&dispatch_info,
&post_info,
SMALL_FEE,
0,
quoted_fee
));

assert_eq!(Assets::total_balance(&XOR.into(), &alice()).unwrap(), 0);
});
}

/// Fee should be postponed until after the transaction
#[test]
fn fee_payment_postpone_failed_xorless_transfer() {
ext().execute_with(|| {
set_weight_to_fee_multiplier(1);
increase_balance(alice(), VAL.into(), balance!(1000));

increase_balance(bob(), XOR.into(), balance!(1000));
increase_balance(bob(), VAL.into(), balance!(1000));

ensure_pool_initialized(XOR.into(), VAL.into());
PoolXYK::deposit_liquidity(
RuntimeOrigin::signed(bob()),
0,
XOR.into(),
VAL.into(),
balance!(500),
balance!(500),
balance!(450),
balance!(450),
)
.unwrap();

fill_spot_price();

let dispatch_info = info_from_weight(Weight::from_parts(100_000_000, 0));

let call = RuntimeCall::LiquidityProxy(liquidity_proxy::Call::xorless_transfer {
dex_id: 0,
asset_id: VAL,
// Swap with desired output may return less tokens than requested
desired_xor_amount: SMALL_FEE + 1,
max_amount_in: 1,
amount: balance!(10),
selected_source_types: vec![],
filter_mode: FilterMode::Disabled,
receiver: bob(),
});

assert_err!(
xor_fee::Pallet::<Runtime>::withdraw_fee(&alice(), &call, &dispatch_info, SMALL_FEE, 0),
TransactionValidityError::Invalid(InvalidTransaction::Payment)
);

let call = RuntimeCall::LiquidityProxy(liquidity_proxy::Call::xorless_transfer {
dex_id: 0,
asset_id: VAL,
// Swap with desired output may return less tokens than requested
desired_xor_amount: 0,
max_amount_in: 0,
amount: balance!(500),
selected_source_types: vec![],
filter_mode: FilterMode::Disabled,
receiver: bob(),
});

assert_err!(
xor_fee::Pallet::<Runtime>::withdraw_fee(&alice(), &call, &dispatch_info, SMALL_FEE, 0),
TransactionValidityError::Invalid(InvalidTransaction::Payment)
);
});
}
Loading