Skip to content

Commit 139ded5

Browse files
authored
Merge pull request #1377 from galacticcouncil/withdraw-limit-chore
feat: configurable global-withdraw-limit
2 parents 7f65319 + d181b39 commit 139ded5

49 files changed

Lines changed: 1413 additions & 831 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

integration-tests/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "runtime-integration-tests"
3-
version = "1.70.0"
3+
version = "1.70.1"
44
description = "Integration tests"
55
authors = ["GalacticCouncil"]
66
edition = "2021"

integration-tests/src/evm_permit.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use frame_support::{assert_noop, assert_ok, sp_runtime::codec::Encode};
1313
use frame_system::RawOrigin;
1414
use hydradx_adapters::price::ConvertBalance;
1515
use hydradx_runtime::evm::precompiles::{CALLPERMIT, DISPATCH_ADDR};
16-
use hydradx_runtime::types::ShortOraclePrice;
16+
use hydradx_runtime::types::TenMinutesOraclePrice;
1717
use hydradx_runtime::AssetRegistry;
1818
use hydradx_runtime::DOT_ASSET_LOCATION;
1919
use hydradx_runtime::XYK;
@@ -1282,7 +1282,7 @@ fn convert_amount_should_work_when_converting_insufficient_to_sufficient_asset()
12821282
));
12831283

12841284
//Convert insufficient to sufficient (WETH)
1285-
type Convert = ConvertBalance<ShortOraclePrice, XykPaymentAssetSupport, DotAssetId>;
1285+
type Convert = ConvertBalance<TenMinutesOraclePrice, XykPaymentAssetSupport, DotAssetId>;
12861286

12871287
let insufficient_amount = 10 * UNITS;
12881288
let amount_in_sufficient =
@@ -1389,7 +1389,7 @@ fn convert_amount_should_fail_gracefully_when_no_xyk_pool_for_fee_payment_asset(
13891389
));
13901390

13911391
//Convert insufficient to sufficient (WETH) should fail as no corresponding XYK pool
1392-
type Convert = ConvertBalance<ShortOraclePrice, XykPaymentAssetSupport, DotAssetId>;
1392+
type Convert = ConvertBalance<TenMinutesOraclePrice, XykPaymentAssetSupport, DotAssetId>;
13931393

13941394
let insufficient_amount = 10 * UNITS;
13951395
let amount_in_weth = Convert::convert((insufficient_asset, WETH, insufficient_amount));
@@ -1478,7 +1478,7 @@ fn convert_amount_should_work_when_converting_sufficient_to_insufficient_asset()
14781478
));
14791479

14801480
//Convert sufficient (HDX) to insufficient
1481-
type Convert = ConvertBalance<ShortOraclePrice, XykPaymentAssetSupport, DotAssetId>;
1481+
type Convert = ConvertBalance<TenMinutesOraclePrice, XykPaymentAssetSupport, DotAssetId>;
14821482

14831483
let sufficient_amount = 10 * UNITS;
14841484
let amount_in_insufficient_asset =

integration-tests/src/exchange_asset.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1244,10 +1244,11 @@ mod circuit_breaker {
12441244
pretty_assertions::assert_eq!(*asset_id, aca, "Trapped asset ID is not ACA");
12451245

12461246
if let polkadot_xcm::v5::Fungibility::Fungible(trapped_amount) = asset.fun {
1247+
let tolerance = sp_std::cmp::max(200_000_000_000u128, expected_amount / 1_000u128); // max(2e11, 0.1%)
12471248
test_utils::assert_eq_approx!(
12481249
trapped_amount,
12491250
expected_amount,
1250-
100000000000,
1251+
tolerance,
12511252
"The trapped asset amount is different than expected"
12521253
);
12531254
}

integration-tests/src/global_withdraw_limit.rs

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,16 @@ use crate::evm::init_omnipool_with_oracle_for_block_10;
44
use crate::polkadot_test_net::*;
55
use frame_support::weights::Weight;
66
use frame_support::{assert_err, assert_noop, assert_ok};
7-
use hydradx_runtime::{AssetRegistry, CircuitBreaker, RuntimeCall, DOT_ASSET_LOCATION};
7+
use hydradx_runtime::{AssetRegistry, CircuitBreaker, RuntimeCall, DOT_ASSET_LOCATION, EVM};
88
use orml_traits::MultiCurrency;
9+
use pallet_circuit_breaker::types::GlobalWithdrawLimitParameters;
910
use pallet_circuit_breaker::GlobalAssetCategory;
1011
use pallet_transaction_payment::OnChargeTransaction;
1112
use polkadot_xcm::v5::prelude::*;
1213
use polkadot_xcm::{VersionedAssetId, VersionedXcm};
1314
use primitives::constants::time::unix_time::DAY;
1415
use primitives::constants::time::MILLISECS_PER_BLOCK;
16+
use sp_core::U256;
1517
use sp_runtime::traits::Dispatchable;
1618
use sp_runtime::FixedU128;
1719
use xcm_emulator::TestExt;
@@ -82,11 +84,23 @@ fn set_dot_external_and_get_transfer_call() -> hydradx_runtime::RuntimeCall {
8284
})
8385
}
8486

87+
fn init_global_withdraw_limit_params() {
88+
let params = GlobalWithdrawLimitParameters {
89+
limit: 1_000_000 * UNITS,
90+
window: DAY,
91+
};
92+
assert_ok!(CircuitBreaker::set_global_withdraw_limit_params(
93+
hydradx_runtime::RuntimeOrigin::root(),
94+
params
95+
));
96+
}
97+
8598
#[test]
8699
fn polkadot_xcm_execute_should_fail_when_lockdown_active_and_asset_is_egress() {
87100
TestNet::reset();
88101
Hydra::execute_with(|| {
89102
// Arrange
103+
init_global_withdraw_limit_params();
90104
let now = CircuitBreaker::timestamp_now();
91105

92106
assert_ok!(CircuitBreaker::set_asset_category(
@@ -152,6 +166,8 @@ fn xtokens_transfer_should_fail_when_lockdown_active_and_asset_is_egress() {
152166
TestNet::reset();
153167
Hydra::execute_with(|| {
154168
// Arrange
169+
init_global_withdraw_limit_params();
170+
155171
let now = CircuitBreaker::timestamp_now();
156172

157173
assert_ok!(CircuitBreaker::set_asset_category(
@@ -195,6 +211,7 @@ fn on_charge_transaction_skips_global_withdraw_accounting_for_native_asset() {
195211
TestNet::reset();
196212
Hydra::execute_with(|| {
197213
// Arrange
214+
init_global_withdraw_limit_params();
198215
let alice: AccountId = ALICE.into();
199216

200217
// Ensure HDX is a participating asset for the global-withdraw logic
@@ -254,6 +271,7 @@ fn on_charge_transaction_skips_global_withdraw_accounting_for_external_asset() {
254271
TestNet::reset();
255272
Hydra::execute_with(|| {
256273
init_omnipool_with_oracle_for_block_10();
274+
init_global_withdraw_limit_params();
257275

258276
// Arrange
259277
let alice: AccountId = ALICE.into();
@@ -321,12 +339,83 @@ fn on_charge_transaction_skips_global_withdraw_accounting_for_external_asset() {
321339
});
322340
}
323341

342+
#[test]
343+
fn evm_on_charge_transaction_skips_global_withdraw_accounting() {
344+
TestNet::reset();
345+
Hydra::execute_with(|| {
346+
init_omnipool_with_oracle_for_block_10();
347+
init_global_withdraw_limit_params();
348+
349+
// Arrange
350+
let evm_addr = evm_address();
351+
let evm_account = hydradx_runtime::EVMAccounts::truncated_account_id(evm_addr);
352+
353+
// Ensure WETH has sufficient balance for the EVM account
354+
assert_ok!(Currencies::deposit(WETH, &evm_account, 100 * UNITS));
355+
356+
// Set WETH as External to make it a participating asset
357+
assert_ok!(CircuitBreaker::set_asset_category(
358+
hydradx_runtime::RuntimeOrigin::root(),
359+
WETH,
360+
Some(GlobalAssetCategory::External)
361+
));
362+
363+
// Activate global lockdown
364+
let now = CircuitBreaker::timestamp_now();
365+
assert_ok!(CircuitBreaker::set_global_withdraw_lockdown(
366+
hydradx_runtime::RuntimeOrigin::root(),
367+
now + 1000
368+
));
369+
370+
let initial_balance = Currencies::free_balance(WETH, &evm_account);
371+
372+
// Act - Make an EVM call that will charge fees
373+
// Simple transfer to self with some data
374+
assert_ok!(EVM::call(
375+
evm_signed_origin(evm_addr),
376+
evm_addr,
377+
evm_addr,
378+
vec![1, 2, 3],
379+
U256::from(0),
380+
100000,
381+
crate::evm::gas_price(),
382+
None,
383+
Some(U256::zero()),
384+
[].into(),
385+
vec![],
386+
));
387+
388+
// Assert
389+
// Fee should be charged from WETH balance
390+
let after_balance = Currencies::free_balance(WETH, &evm_account);
391+
assert!(after_balance < initial_balance, "EVM fee should be charged");
392+
393+
// Verify global-withdraw accounting was skipped for the EVM fee withdraw
394+
assert_eq!(CircuitBreaker::withdraw_limit_accumulator().0, 0);
395+
396+
// Also assert lockdown is still active
397+
assert!(CircuitBreaker::withdraw_lockdown_until().is_some());
398+
399+
// Negative control: normal (non-fee) operations with participating asset are blocked during lockdown
400+
assert_err!(
401+
Currencies::withdraw(
402+
WETH,
403+
&evm_account,
404+
1 * UNITS,
405+
frame_support::traits::ExistenceRequirement::AllowDeath
406+
),
407+
pallet_circuit_breaker::Error::<hydradx_runtime::Runtime>::WithdrawLockdownActive
408+
);
409+
});
410+
}
411+
324412
#[test]
325413
fn xcm_transfer_assets_blocked_during_lockdown() {
326414
TestNet::reset();
327415
Hydra::execute_with(|| {
328416
// Arrange
329417
init_omnipool_with_oracle_for_block_10();
418+
init_global_withdraw_limit_params();
330419

331420
let now = CircuitBreaker::timestamp_now();
332421
let until = now + MILLISECS_PER_BLOCK * 11;
@@ -351,9 +440,11 @@ fn lockdown_expiry_allows_egress() {
351440
TestNet::reset();
352441
Hydra::execute_with(|| {
353442
// Arrange
443+
init_omnipool_with_oracle_for_block_10();
444+
init_global_withdraw_limit_params();
445+
354446
let now = CircuitBreaker::timestamp_now();
355447
let until = now + MILLISECS_PER_BLOCK * 11;
356-
init_omnipool_with_oracle_for_block_10();
357448

358449
assert_ok!(CircuitBreaker::set_global_withdraw_lockdown(
359450
hydradx_runtime::RuntimeOrigin::root(),
@@ -384,6 +475,8 @@ fn lockdown_expiry_allows_egress() {
384475
fn withdraw_external_should_be_accounted() {
385476
TestNet::reset();
386477
Hydra::execute_with(|| {
478+
init_global_withdraw_limit_params();
479+
387480
// Set HDX as External to avoid conversion issues
388481
assert_ok!(CircuitBreaker::set_asset_category(
389482
hydradx_runtime::RuntimeOrigin::root(),
@@ -440,14 +533,15 @@ fn transfer_to_sink_should_be_accounted_for_participating_assets() {
440533
TestNet::reset();
441534
Hydra::execute_with(|| {
442535
init_omnipool_with_oracle_for_block_10();
536+
init_global_withdraw_limit_params();
443537

444538
let sink: AccountId = [99u8; 32].into();
445539
assert_ok!(CircuitBreaker::add_egress_accounts(
446540
hydradx_runtime::RuntimeOrigin::root(),
447541
vec![sink.clone()]
448542
));
449543

450-
let amount = 100 * UNITS;
544+
let amount = 10 * UNITS;
451545

452546
// 1. External -> Accounted (Override DOT to External)
453547
assert_ok!(CircuitBreaker::set_asset_category(
@@ -495,6 +589,7 @@ fn should_account_withdraw_operation_accounts_external_and_local() {
495589
TestNet::reset();
496590
Hydra::execute_with(|| {
497591
init_omnipool_with_oracle_for_block_10();
592+
init_global_withdraw_limit_params();
498593

499594
let who: AccountId = ALICE.into();
500595
let amount = 10 * UNITS;
@@ -580,6 +675,8 @@ fn ingress_deposit_decrements_accumulator_for_external() {
580675
// External deposits are always accounted as ingress and decrement the accumulator.
581676
TestNet::reset();
582677
Hydra::execute_with(|| {
678+
init_global_withdraw_limit_params();
679+
583680
let who: AccountId = ALICE.into();
584681
let amount = 10 * UNITS;
585682

@@ -620,6 +717,8 @@ fn ingress_deposit_decrements_accumulator_for_local_only_when_source_is_egress()
620717
// Local deposits are accounted only when `maybe_from` is an egress account.
621718
TestNet::reset();
622719
Hydra::execute_with(|| {
720+
init_global_withdraw_limit_params();
721+
623722
let alice: AccountId = ALICE.into();
624723
let egress_src: AccountId = [9u8; 32].into();
625724
let non_egress_src: AccountId = [10u8; 32].into();
@@ -684,6 +783,7 @@ fn dot_external_limit_trigger_fails_then_decays_to_zero() {
684783
TestNet::reset();
685784
Hydra::execute_with(|| {
686785
init_omnipool_with_oracle_for_block_10();
786+
init_global_withdraw_limit_params();
687787
assert_ok!(CircuitBreaker::reset_withdraw_lockdown(
688788
hydradx_runtime::RuntimeOrigin::root()
689789
));
@@ -718,9 +818,12 @@ fn dot_external_limit_trigger_fails_then_decays_to_zero() {
718818
);
719819

720820
// Set a limit that allows the first withdraw but blocks the next one.
721-
assert_ok!(CircuitBreaker::set_global_withdraw_limit(
821+
assert_ok!(CircuitBreaker::set_global_withdraw_limit_params(
722822
hydradx_runtime::RuntimeOrigin::root(),
723-
acc_after_first + 1
823+
pallet_circuit_breaker::types::GlobalWithdrawLimitParameters {
824+
limit: acc_after_first + 1,
825+
window: DAY
826+
}
724827
));
725828

726829
assert_err!(
@@ -753,6 +856,7 @@ fn dot_external_lockdown_blocks_withdraw_but_regular_dot_transfer_still_works()
753856
TestNet::reset();
754857
Hydra::execute_with(|| {
755858
init_omnipool_with_oracle_for_block_10();
859+
init_global_withdraw_limit_params();
756860
assert_ok!(CircuitBreaker::reset_withdraw_lockdown(
757861
hydradx_runtime::RuntimeOrigin::root()
758862
));

pallets/circuit-breaker/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "pallet-circuit-breaker"
3-
version = "1.5.0"
3+
version = "1.5.1"
44
authors = ["GalacticCouncil <hydradx@galacticcouncil.io>"]
55
edition = "2021"
66
license = "Apache-2.0"

pallets/circuit-breaker/src/benchmarking.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,11 +161,11 @@ benchmarks! {
161161
assert_eq!(LiquidityRemoveLimitPerAsset::<T>::get(asset_id), trade_limit);
162162
}
163163

164-
set_global_withdraw_limit {
165-
let balance = T::Balance::from(1_000_000u32);
166-
}: _(RawOrigin::Root, balance)
164+
set_global_withdraw_limit_params {
165+
let params = GlobalWithdrawLimitParameters { limit: T::Balance::from(1_000_000u32), window: 86_400_000u32.into() };
166+
}: _(RawOrigin::Root, params.clone())
167167
verify {
168-
assert_eq!(crate::Pallet::<T>::global_withdraw_limit(), Some(balance));
168+
assert_eq!(crate::Pallet::<T>::global_withdraw_limit_config(), Some(params));
169169
}
170170

171171
reset_withdraw_lockdown {

0 commit comments

Comments
 (0)