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
6 changes: 3 additions & 3 deletions Cargo.lock

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

2 changes: 1 addition & 1 deletion integration-tests/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "runtime-integration-tests"
version = "1.104.0"
version = "1.105.0"
description = "Integration tests"
authors = ["GalacticCouncil"]
edition = "2021"
Expand Down
Binary file added integration-tests/dca-snapshot/SNAPSHOT_13260776
Binary file not shown.
89 changes: 89 additions & 0 deletions integration-tests/src/dca.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4944,6 +4944,95 @@ mod aave_atoken {
});
}

#[test]
fn router_trading_limit_reached_should_be_retriable() {
use frame_support::traits::Contains;
assert!(hydradx_runtime::RetryOnErrorForDca::contains(
&pallet_route_executor::Error::<Runtime>::TradingLimitReached.into()
));
}

// Snapshot at block 13260776, produced with the same scraper command as above,
// --at 0x798bc20aeb759a30dd97aafdab03346ff463cbc17e776216d171a8b3ad6411d6
const PATH_TO_HSM_WINDDOWN_SNAPSHOT: &str = "dca-snapshot/SNAPSHOT_13260776";

// replays mainnet block 13260777: hsm wind-down schedule 33794 (sUSDe->HOLLAR->aUSDT) failed
// with router TradingLimitReached and was terminated instead of retried
#[test]
fn dca_should_retry_when_router_trading_limit_reached() {
TestNet::reset();

hydra_live_ext(PATH_TO_HSM_WINDDOWN_SNAPSHOT).execute_with(|| {
//Arrange
assert_eq!(hydradx_runtime::System::block_number(), 13260776);
let schedule_id = 33794;
assert!(DCA::schedules(schedule_id).is_some());
// on mainnet the timestamp inherent runs after on_initialize — keep the parent timestamp
hydradx_runtime::System::set_block_number(13260777);

// the slim snapshot strips schedule 33812's owner, so replay its trade by hand:
// the aave hop moves pool-111 shares out of the aHUSDT contract account, then the
// stableswap hop removes liquidity from pool 111 right before 33794 executes
let ahusdt: sp_runtime::AccountId32 =
hex_literal::hex!("455448001806860d27ee903c1ec7586d4f7d598d7591f1240000000000000000").into();
assert_ok!(Currencies::update_balance(
RuntimeOrigin::root(),
ahusdt,
111,
-22_133_969_015_170_432_881i128,
));
assert_ok!(Currencies::update_balance(
RuntimeOrigin::root(),
BOB.into(),
111,
22_133_969_015_170_432_881i128,
));
assert_ok!(Router::sell(
RuntimeOrigin::signed(BOB.into()),
111,
222,
22_133_969_015_170_432_881u128,
0,
vec![Trade {
pool: PoolType::Stableswap(111),
asset_in: 111,
asset_out: 222,
}]
.try_into()
.unwrap(),
));
// exact hollar amount of the mainnet swap — the replayed state matches
assert_eq!(Currencies::free_balance(222, &BOB.into()), 22_567_968_483_370_805_906);

//Act
DCA::on_initialize(13260777);

//Assert: trade failed exactly like mainnet, but got retried instead of terminated
assert_trade_failed_with_router_trading_limit_reached(schedule_id);
assert!(
DCA::schedules(schedule_id).is_some(),
"schedule must be retried, not terminated"
);
assert_eq!(DCA::retries_on_error(schedule_id), 1);
});
}

fn assert_trade_failed_with_router_trading_limit_reached(schedule_id: u32) {
let expected: sp_runtime::DispatchError = pallet_route_executor::Error::<Runtime>::TradingLimitReached.into();
let events = last_hydra_events(20);
let found = events.iter().any(|e| {
matches!(
e,
RuntimeEvent::DCA(pallet_dca::Event::TradeFailed { id, error, .. })
if *id == schedule_id && *error == expected
)
});
assert!(
found,
"expected TradeFailed event with router::TradingLimitReached for schedule {schedule_id}"
);
}

fn assert_trade_failed_with_omnipool_insufficient_balance(schedule_id: u32) {
let expected: sp_runtime::DispatchError = pallet_omnipool::Error::<Runtime>::InsufficientBalance.into();
let events = last_hydra_events(20);
Expand Down
2 changes: 1 addition & 1 deletion pallets/dca/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = 'pallet-dca'
version = "1.18.1"
version = "1.18.2"
description = 'A pallet to manage DCA scheduling'
authors = ['GalacticCouncil']
edition = '2021'
Expand Down
17 changes: 16 additions & 1 deletion pallets/dca/src/tests/mock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ pub const DAI: AssetId = 2;
pub const BTC: AssetId = 3;
pub const FORBIDDEN_ASSET: AssetId = 4;
pub const DOT: AssetId = 5;
pub const RETRY_ON_ERROR_ASSET: AssetId = 6;
pub const REGISTERED_ASSET: AssetId = 1000;
pub const ONE_HUNDRED_BLOCKS: BlockNumber = 100;

Expand Down Expand Up @@ -454,6 +455,12 @@ impl TradeExecution<OriginForRuntime, AccountId, AssetId, Balance> for OmniPool
return Err(ExecutorError::Error(pallet_omnipool::Error::<Test>::NotAllowed.into()));
}

if asset_in == RETRY_ON_ERROR_ASSET {
return Err(ExecutorError::Error(
pallet_route_executor::Error::<Test>::TradingLimitReached.into(),
));
}

SELL_EXECUTIONS.with(|v| {
let mut m = v.borrow_mut();
m.push(SellExecution {
Expand Down Expand Up @@ -696,13 +703,21 @@ impl Config for Test {
type AmmTradeWeights = ();
type MinimumTradingLimit = MinTradeAmount;
type NativePriceOracle = NativePriceOracleMock;
type RetryOnError = ();
type RetryOnError = RetryOnErrorMock;
type PolkadotNativeAssetId = PolkadotNativeCurrencyId;
type SwappablePaymentAssetSupport = MockedInsufficientAssetSupport;
type ExtraGasSupport = ExtraGasSetterMock;
type GasWeightMapping = MockGasWeightMapping;
}

pub struct RetryOnErrorMock;

impl frame_support::traits::Contains<DispatchError> for RetryOnErrorMock {
fn contains(t: &DispatchError) -> bool {
*t == pallet_route_executor::Error::<Test>::TradingLimitReached.into()
}
}

pub struct ExtraGasSetterMock;

impl ExtraGasSupport for ExtraGasSetterMock {
Expand Down
47 changes: 47 additions & 0 deletions pallets/dca/src/tests/on_initialize.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,53 @@ fn dca_schedule_should_terminate_when_error_is_not_configured_to_continue_on() {
});
}

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

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

assert_ok!(DCA::schedule(RuntimeOrigin::signed(ALICE), schedule, Option::None));

//Act and assert
let schedule_id = 0;
set_to_blocknumber(502);
assert!(DCA::schedules(schedule_id).is_some());
assert_eq!(DCA::retries_on_error(schedule_id), 1);
assert_scheduled_ids!(522, vec![schedule_id]);

set_to_blocknumber(522);
assert_eq!(DCA::retries_on_error(schedule_id), 2);
assert_scheduled_ids!(562, vec![schedule_id]);

set_to_blocknumber(562);
assert_eq!(DCA::retries_on_error(schedule_id), 3);
assert_scheduled_ids!(642, vec![schedule_id]);

set_to_blocknumber(642);
assert_number_of_executed_sell_trades!(0);
assert_that_dca_is_terminated(ALICE, schedule_id, Error::<Test>::MaxRetryReached.into());
});
}

#[test]
fn dca_schedule_should_continue_on_multiple_failures_then_terminated() {
ExtBuilder::default()
Expand Down
2 changes: 1 addition & 1 deletion runtime/hydradx/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "hydradx-runtime"
version = "433.0.0"
version = "434.0.0"
authors = ["GalacticCouncil"]
edition = "2021"
license = "Apache 2.0"
Expand Down
3 changes: 3 additions & 0 deletions runtime/hydradx/src/assets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -942,6 +942,9 @@ impl Contains<DispatchError> for RetryOnErrorForDca {
// liquidity index is timestamp-dependent, so the rounding boundary
// shifts and a later attempt may pass.
pallet_omnipool::Error::<Runtime>::InsufficientBalance.into(),
// same class as dca's own retriable TradeLimitReached — erc20/aToken rounding
// can undershoot the dry-run output passed as router min limit
pallet_route_executor::Error::<Runtime>::TradingLimitReached.into(),
Comment on lines +945 to +947
pallet_dispatcher::Error::<Runtime>::EvmOutOfGas.into(),
pallet_circuit_breaker::Error::<Runtime>::DepositLimitExceededForWhitelistedAccount.into(),
];
Expand Down
2 changes: 1 addition & 1 deletion runtime/hydradx/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion {
spec_name: Cow::Borrowed("hydradx"),
impl_name: Cow::Borrowed("hydradx"),
authoring_version: 1,
spec_version: 433,
spec_version: 434,
impl_version: 0,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
Expand Down
Loading