Skip to content

Commit

Permalink
Merge pull request #983 from galacticcouncil/rolling_dca
Browse files Browse the repository at this point in the history
feat: rolling dca
  • Loading branch information
mrq1911 authored Jan 20, 2025
2 parents fd40770 + 1b39f9f commit 9ff87be
Show file tree
Hide file tree
Showing 6 changed files with 109 additions and 29 deletions.
8 changes: 0 additions & 8 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 0 additions & 8 deletions integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,9 @@ hydradx-traits = { workspace = true }
hydra-dx-math = { workspace = true }
pallet-transaction-multi-payment = { workspace = true, features = ["evm"] }
pallet-currencies = { workspace = true }
pallet-duster = { workspace = true }
pallet-ema-oracle = { workspace = true }
warehouse-liquidity-mining = { workspace = true }
pallet-otc = { workspace = true }
pallet-otc-settlements = { workspace = true }
pallet-relaychain-info = { workspace = true }
pallet-route-executor = { workspace = true }
pallet-dca = { workspace = true }
Expand Down Expand Up @@ -82,9 +80,7 @@ cumulus-pallet-parachain-system = { workspace = true }
cumulus-pallet-xcm = { workspace = true }
cumulus-pallet-xcmp-queue = { workspace = true }
cumulus-primitives-core = { workspace = true }
cumulus-primitives-utility = { workspace = true }
cumulus-primitives-parachain-inherent = { workspace = true }
cumulus-primitives-timestamp = { workspace = true }
staging-parachain-info = { workspace = true }
cumulus-test-relay-sproof-builder = { workspace = true }

Expand All @@ -96,11 +92,9 @@ xcm-executor = { workspace = true }
polkadot-xcm = { workspace = true }

# Substrate dependencies
frame-benchmarking = { workspace = true, optional = true }
frame-executive = { workspace = true }
frame-support = { workspace = true }
frame-system = { workspace = true }
frame-system-benchmarking = { workspace = true, optional = true }
frame-system-rpc-runtime-api = { workspace = true }
pallet-aura = { workspace = true }
pallet-balances = { workspace = true }
Expand All @@ -124,7 +118,6 @@ sp-session = { workspace = true }
sp-std = { workspace = true }
sp-transaction-pool = { workspace = true }
sp-version = { workspace = true }
sp-staking = { workspace = true }
sp-trie = { workspace = true }
sp-io = { workspace = true }
sp-consensus-babe = { workspace = true }
Expand All @@ -149,7 +142,6 @@ pallet-relaychain-info = { workspace = true }
xcm-emulator = { workspace = true }
test-utils = { workspace = true }
libsecp256k1 = { workspace = true }
proptest = "1.5.0"


[features]
Expand Down
30 changes: 29 additions & 1 deletion integration-tests/src/dca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const TREASURY_ACCOUNT_INIT_BALANCE: Balance = 1000 * UNITS;
mod omnipool {
use super::*;
use frame_support::assert_ok;
use hydradx_runtime::{DCA, XYK};
use hydradx_runtime::{Balances, Currencies, DCA, XYK};
use hydradx_traits::router::{PoolType, Trade};
use hydradx_traits::AssetKind;
use pallet_broadcast::types::Destination;
Expand Down Expand Up @@ -653,6 +653,34 @@ mod omnipool {
});
}

#[test]
fn rolling_buy_dca_should_continue_until_funds_are_spent() {
TestNet::reset();
Hydra::execute_with(|| {
//Arrange
init_omnipool_with_oracle_for_block_10();
let balance = 20000 * UNITS;
let trade_size = 500 * UNITS;
let dca_budget = 0; // rolling
Balances::force_set_balance(RuntimeOrigin::root(), ALICE.into(), balance).unwrap();
create_schedule(
ALICE,
schedule_fake_with_buy_order(PoolType::Omnipool, HDX, DAI, trade_size, dca_budget),
);
let reserved = Balances::reserved_balance(&ALICE.into());
assert!(Balances::free_balance(&ALICE.into()) <= balance - reserved);
let dai_balance = Currencies::free_balance(DAI, &ALICE.into());

//Act
run_to_block(11, 150);

//Assert
assert!(Balances::free_balance(&ALICE.into()) > reserved);
assert!(Currencies::free_balance(DAI, &ALICE.into()) > dai_balance);
assert!(DCA::schedules(0).is_none());
});
}

#[test]
fn sell_schedule_execution_should_work_when_block_is_initialized() {
TestNet::reset();
Expand Down
39 changes: 27 additions & 12 deletions pallets/dca/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@
//! In case the given block is full, the execution will be scheduled for the subsequent block.
//!
//! Upon creating a schedule, the user specifies a budget (`total_amount`) that will be reserved.
//! `total_amount` can be zero, in which case the schedule will be executed until it is terminated.
//! The currency of this reservation is the sold (`amount_in`) currency.
//!
//! ### Executing a Schedule
//!
//! Orders are executed during block initialization and are sorted based on randomness derived from the relay chain block hash.
//!
//! A trade is executed and replanned as long as there is remaining budget from the initial allocation.
//! When the `total_amount` is not zero, trades are executed as long as there is budget remaining from the initial allocation.
//!
//! For both successful and failed trades, a fee is deducted from the schedule owner.
//! The fee is deducted in the sold (`amount_in`) currency.
Expand Down Expand Up @@ -452,7 +453,8 @@ pub mod pallet {
/// The reservation currency will be the `amount_in` currency of the order.
///
/// Trades are executed as long as there is budget remaining
/// from the initial `total_amount` allocation.
/// from the initial `total_amount` allocation, unless `total_amount` is 0, then trades
/// are executed until schedule is terminated.
///
/// If a trade fails due to slippage limit or price stability errors, it will be retried.
/// If the number of retries reaches the maximum allowed,
Expand Down Expand Up @@ -482,10 +484,6 @@ pub mod pallet {
schedule.order.get_asset_in(),
T::MinBudgetInNativeCurrency::get(),
)?;
ensure!(
schedule.total_amount >= min_budget,
Error::<T>::TotalAmountIsSmallerThanMinBudget
);
ensure!(
schedule.period >= BlockNumberFor::<T>::from(T::MinimalPeriod::get()),
Error::<T>::PeriodTooShort
Expand Down Expand Up @@ -518,10 +516,23 @@ pub mod pallet {
);

let amount_in_with_transaction_fee = amount_in.saturating_add(transaction_fee).saturating_mul(2);
ensure!(
amount_in_with_transaction_fee <= schedule.total_amount,
Error::<T>::BudgetTooLow
);
let reserve_amount = if schedule.is_rolling() {
ensure!(
amount_in_with_transaction_fee >= min_budget,
Error::<T>::MinTradeAmountNotReached
);
amount_in_with_transaction_fee
} else {
ensure!(
schedule.total_amount >= min_budget,
Error::<T>::TotalAmountIsSmallerThanMinBudget
);
ensure!(
amount_in_with_transaction_fee <= schedule.total_amount,
Error::<T>::BudgetTooLow
);
schedule.total_amount
};

let next_schedule_id =
ScheduleIdSequencer::<T>::try_mutate(|current_id| -> Result<ScheduleId, DispatchError> {
Expand All @@ -532,14 +543,14 @@ pub mod pallet {

Schedules::<T>::insert(next_schedule_id, &schedule);
ScheduleOwnership::<T>::insert(who.clone(), next_schedule_id, ());
RemainingAmounts::<T>::insert(next_schedule_id, schedule.total_amount);
RemainingAmounts::<T>::insert(next_schedule_id, reserve_amount);
RetriesOnError::<T>::insert(next_schedule_id, 0);

T::Currencies::reserve_named(
&T::NamedReserveId::get(),
schedule.order.get_asset_in(),
&who,
schedule.total_amount,
reserve_amount,
)?;

let blocknumber_for_first_schedule_execution = Self::get_first_execution_block(start_execution_block)?;
Expand Down Expand Up @@ -930,6 +941,10 @@ impl<T: Config> Pallet<T> {
schedule: &Schedule<T::AccountId, T::AssetId, BlockNumberFor<T>>,
amount_to_unreserve: Balance,
) -> DispatchResult {
if schedule.is_rolling() {
return Ok(());
};

RemainingAmounts::<T>::try_mutate_exists(schedule_id, |maybe_remaining_amount| -> DispatchResult {
let remaining_amount = maybe_remaining_amount
.as_mut()
Expand Down
46 changes: 46 additions & 0 deletions pallets/dca/src/tests/on_initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -848,6 +848,52 @@ fn full_sell_dca_should_be_completed_when_exact_total_amount_specified_for_the_t
});
}

#[test]
fn rolling_dca_should_end_when_account_has_no_balance() {
ExtBuilder::default()
.with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE)])
.build()
.execute_with(|| {
//Arrange
proceed_to_blocknumber(1, 500);

let amount_to_sell = *AMOUNT_OUT_FOR_OMNIPOOL_SELL;

let schedule = ScheduleBuilder::new()
.with_total_amount(0)
.with_period(ONE_HUNDRED_BLOCKS)
.with_order(Order::Sell {
asset_in: HDX,
asset_out: BTC,
amount_in: amount_to_sell,
min_amount_out: Balance::MIN,
route: create_bounded_vec(vec![Trade {
pool: Omnipool,
asset_in: HDX,
asset_out: BTC,
}]),
})
.build();

assert_eq!(schedule.is_rolling(), true);
assert_ok!(DCA::schedule(RuntimeOrigin::signed(ALICE), schedule, None));
assert_eq!(
(*AMOUNT_OUT_FOR_OMNIPOOL_SELL + get_fee_for_sell_in_hdx()).saturating_mul(2),
Currencies::reserved_balance(HDX, &ALICE)
);

//Act
proceed_to_blocknumber(501, 801);

//Assert
assert_eq!(0, Currencies::reserved_balance(HDX, &ALICE));
assert_number_of_executed_sell_trades!(3);

let schedule_id = 0;
assert_that_dca_is_terminated(ALICE, schedule_id, sp_runtime::TokenError::FundsUnavailable.into());
});
}

#[test]
fn full_buy_dca_should_be_completed_when_some_execution_is_successful_but_not_enough_balance() {
let alice_init_hdx_balance = 10000 * ONE;
Expand Down
7 changes: 7 additions & 0 deletions pallets/dca/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub struct Schedule<AccountId, AssetId, BlockNumber> {
/// The time period (in blocks) between two schedule executions.
pub period: BlockNumber,
/// The total amount (budget) the user wants to spend on the whole DCA.
/// Can be set to zero, in which case the schedule will run indefinitely.
/// Its currency is the sold (amount_in) currency specified in `order`.
pub total_amount: Balance,
/// The maximum number of retries in case of failing schedules.
Expand All @@ -34,6 +35,12 @@ pub struct Schedule<AccountId, AssetId, BlockNumber> {
pub order: Order<AssetId>,
}

impl<AccountId, AssetId, BlockNumber> Schedule<AccountId, AssetId, BlockNumber> {
pub fn is_rolling(&self) -> bool {
self.total_amount == 0
}
}

#[derive(Encode, Decode, Debug, Eq, PartialEq, Clone, TypeInfo, MaxEncodedLen)]
pub enum Order<AssetId> {
Sell {
Expand Down

0 comments on commit 9ff87be

Please sign in to comment.