Skip to content

Commit 8e8061b

Browse files
Merge branch 'master' into gigahdx-lock
2 parents 7eec9df + ee559bc commit 8e8061b

4 files changed

Lines changed: 137 additions & 43 deletions

File tree

integration-tests/src/global_withdraw_limit.rs

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1528,3 +1528,99 @@ fn inbound_xcm_incomplete_message_still_accounts_egress() {
15281528
);
15291529
});
15301530
}
1531+
1532+
#[test]
1533+
fn polkadot_xcm_execute_withdraw_external_asset_succeeds_when_oracle_cannot_price_route() {
1534+
TestNet::reset();
1535+
Hydra::execute_with(|| {
1536+
init_global_withdraw_limit_params();
1537+
1538+
assert_ok!(CircuitBreaker::set_asset_category(
1539+
hydradx_runtime::RuntimeOrigin::root(),
1540+
DOT,
1541+
Some(GlobalAssetCategory::External)
1542+
));
1543+
// XCM asset transactor needs a location -> asset id mapping for DOT.
1544+
assert_ok!(hydradx_runtime::AssetRegistry::set_location(DOT, DOT_ASSET_LOCATION));
1545+
assert_ok!(hydradx_runtime::MultiTransactionPayment::add_currency(
1546+
hydradx_runtime::RuntimeOrigin::root(),
1547+
DOT,
1548+
FixedU128::from_rational(50, 100),
1549+
));
1550+
pallet_transaction_multi_payment::AcceptedCurrencyPrice::<hydradx_runtime::Runtime>::insert(
1551+
DOT,
1552+
FixedU128::from_rational(50, 100),
1553+
);
1554+
1555+
let alice: AccountId = ALICE.into();
1556+
let amount = 10 * UNITS;
1557+
let alice_dot_before = Currencies::free_balance(DOT, &alice);
1558+
assert!(alice_dot_before >= amount);
1559+
1560+
//Act
1561+
let message = xcm_message_withdraw_deposit(Location::parent(), amount);
1562+
let call = RuntimeCall::PolkadotXcm(pallet_xcm::Call::execute {
1563+
message: Box::new(VersionedXcm::from(message)),
1564+
max_weight: Weight::from_parts(1_000_000_000_000, 0),
1565+
});
1566+
1567+
assert_ok!(call.dispatch(hydradx_runtime::RuntimeOrigin::signed(ALICE.into())));
1568+
1569+
//Assert
1570+
assert!(
1571+
Currencies::free_balance(DOT, &alice) <= alice_dot_before - amount,
1572+
"DOT must have been withdrawn from Alice for the outbound XCM; before={alice_dot_before}, after={}",
1573+
Currencies::free_balance(DOT, &alice)
1574+
);
1575+
});
1576+
}
1577+
1578+
#[test]
1579+
fn withdraw_succeeds_for_asset_in_overrides_but_not_in_accepted_currencies() {
1580+
TestNet::reset();
1581+
Hydra::execute_with(|| {
1582+
init_global_withdraw_limit_params();
1583+
init_omnipool_with_oracle_for_block_10();
1584+
1585+
assert_ok!(CircuitBreaker::set_asset_category(
1586+
hydradx_runtime::RuntimeOrigin::root(),
1587+
DOT,
1588+
Some(GlobalAssetCategory::External),
1589+
));
1590+
assert_ok!(hydradx_runtime::AssetRegistry::set_location(DOT, DOT_ASSET_LOCATION));
1591+
1592+
assert!(
1593+
!pallet_transaction_multi_payment::AcceptedCurrencies::<hydradx_runtime::Runtime>::contains_key(DOT),
1594+
"precondition: DOT must not be an accepted fee currency",
1595+
);
1596+
assert!(
1597+
pallet_transaction_multi_payment::AcceptedCurrencyPrice::<hydradx_runtime::Runtime>::get(DOT).is_none(),
1598+
"precondition: DOT must not be in the multi-payment price cache",
1599+
);
1600+
1601+
let alice: AccountId = ALICE.into();
1602+
let amount = 10 * UNITS;
1603+
let alice_dot_before = Currencies::free_balance(DOT, &alice);
1604+
assert!(alice_dot_before >= amount);
1605+
let acc_before = CircuitBreaker::withdraw_limit_accumulator().0;
1606+
1607+
// Act
1608+
assert_ok!(Currencies::withdraw(
1609+
DOT,
1610+
&alice,
1611+
amount,
1612+
frame_support::traits::ExistenceRequirement::AllowDeath,
1613+
));
1614+
1615+
// Assert
1616+
assert_eq!(
1617+
Currencies::free_balance(DOT, &alice),
1618+
alice_dot_before - amount,
1619+
"DOT balance must reflect the withdraw",
1620+
);
1621+
assert!(
1622+
CircuitBreaker::withdraw_limit_accumulator().0 > acc_before,
1623+
"Global accumulator must increase — only the ConvertBalance fallback could have priced this withdraw",
1624+
);
1625+
});
1626+
}

runtime/hydradx/src/assets.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ impl pallet_currencies::Config for Runtime {
429429
type ReserveAccount = ReserveAccount;
430430
type GetNativeCurrencyId = NativeAssetId;
431431
type RegistryInspect = AssetRegistry;
432-
type EgressHandler = circuit_breaker::WithdrawLimitHandler<NativeAssetId>;
432+
type EgressHandler = circuit_breaker::WithdrawLimitHandler;
433433
type WeightInfo = weights::pallet_currencies::HydraWeight<Runtime>;
434434
}
435435

runtime/hydradx/src/circuit_breaker.rs

Lines changed: 38 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -7,33 +7,39 @@ use pallet_asset_registry::AssetType;
77
use pallet_circuit_breaker::types::EgressOperationKind;
88
use pallet_circuit_breaker::GlobalAssetCategory;
99
use primitives::Balance;
10+
use sp_runtime::helpers_128bit::multiply_by_rational_with_rounding;
1011
use sp_runtime::traits::Convert;
11-
use sp_runtime::DispatchResult;
12+
use sp_runtime::{DispatchResult, FixedPointNumber, FixedU128, Rounding};
1213
use sp_std::marker::PhantomData;
1314

14-
pub struct WithdrawLimitHandler<RC>(PhantomData<RC>);
15-
impl<RC: Get<AssetId>> AssetWithdrawHandler<AccountId, AssetId, Balance> for WithdrawLimitHandler<RC> {
16-
type OnWithdraw = OnWithdrawHook<RC>;
17-
type OnDeposit = OnDepositHook<RC>;
18-
type OnTransfer = OnTransferHook<RC>;
15+
pub struct WithdrawLimitHandler;
16+
impl AssetWithdrawHandler<AccountId, AssetId, Balance> for WithdrawLimitHandler {
17+
type OnWithdraw = OnWithdrawHook;
18+
type OnDeposit = OnDepositHook;
19+
type OnTransfer = OnTransferHook;
1920
}
2021

21-
pub struct WithdrawCircuitBreaker<ReferenceCurrencyId>(PhantomData<ReferenceCurrencyId>);
22-
impl<ReferenceCurrencyId: Get<AssetId>> WithdrawCircuitBreaker<ReferenceCurrencyId> {
22+
pub struct WithdrawCircuitBreaker;
23+
impl WithdrawCircuitBreaker {
2324
fn convert_to_hdx(asset_id: AssetId, amount: Balance) -> Result<Balance, DispatchError> {
24-
let ref_currency = ReferenceCurrencyId::get();
25+
let ref_currency = NativeAssetId::get();
2526
if asset_id == ref_currency {
2627
return Ok(amount);
2728
}
2829

29-
let (converted, _) = ConvertBalance::<TenMinutesOraclePrice, XykPaymentAssetSupport, DotAssetId>::convert((
30-
asset_id,
31-
ref_currency,
32-
amount,
33-
))
34-
.ok_or(pallet_circuit_breaker::Error::<Runtime>::FailedToConvertAsset)?;
35-
36-
Ok(converted)
30+
MultiTransactionPayment::currency_price(asset_id)
31+
.and_then(|price| {
32+
multiply_by_rational_with_rounding(amount, FixedU128::DIV, price.into_inner(), Rounding::Up)
33+
})
34+
.or_else(|| {
35+
ConvertBalance::<TenMinutesOraclePrice, XykPaymentAssetSupport, DotAssetId>::convert((
36+
asset_id,
37+
ref_currency,
38+
amount,
39+
))
40+
.map(|(converted, _)| converted)
41+
})
42+
.ok_or_else(|| pallet_circuit_breaker::Error::<Runtime>::FailedToConvertAsset.into())
3743
}
3844

3945
pub fn global_asset_category(asset_id: AssetId) -> Option<GlobalAssetCategory> {
@@ -97,22 +103,18 @@ impl<ReferenceCurrencyId: Get<AssetId>> WithdrawCircuitBreaker<ReferenceCurrency
97103
}
98104
}
99105

100-
pub struct OnWithdrawHook<RC>(PhantomData<RC>);
101-
impl<RC: Get<AssetId>> orml_traits::Handler<(AssetId, Balance)> for OnWithdrawHook<RC> {
106+
pub struct OnWithdrawHook;
107+
impl orml_traits::Handler<(AssetId, Balance)> for OnWithdrawHook {
102108
fn handle(t: &(AssetId, Balance)) -> DispatchResult {
103109
// `who` is not used: in XCM path all withdrawals go to buffer regardless of origin;
104110
// in non-XCM path Withdraw is always accounted for both Local and External assets regardless of who.
105111
let (asset_id, amount) = t;
106112

107-
if !WithdrawCircuitBreaker::<RC>::should_account_withdraw_operation(
108-
*asset_id,
109-
EgressOperationKind::Withdraw,
110-
None,
111-
) {
113+
if !WithdrawCircuitBreaker::should_account_withdraw_operation(*asset_id, EgressOperationKind::Withdraw, None) {
112114
return Ok(());
113115
}
114116

115-
let amount_ref_currency = WithdrawCircuitBreaker::<RC>::convert_to_hdx(*asset_id, *amount)?;
117+
let amount_ref_currency = WithdrawCircuitBreaker::convert_to_hdx(*asset_id, *amount)?;
116118

117119
if let Some(mut buffer) = pallet_circuit_breaker::XcmEgressBuffer::<Runtime>::get() {
118120
buffer.0 = buffer.0.saturating_add(amount_ref_currency);
@@ -125,8 +127,8 @@ impl<RC: Get<AssetId>> orml_traits::Handler<(AssetId, Balance)> for OnWithdrawHo
125127
}
126128
}
127129

128-
pub struct OnTransferHook<RC>(PhantomData<RC>);
129-
impl<RC: Get<AssetId>> orml_traits::currency::OnTransfer<AccountId, AssetId, Balance> for OnTransferHook<RC> {
130+
pub struct OnTransferHook;
131+
impl orml_traits::currency::OnTransfer<AccountId, AssetId, Balance> for OnTransferHook {
130132
fn on_transfer(asset_id: AssetId, from: &AccountId, to: &AccountId, amount: Balance) -> DispatchResult {
131133
let is_from_egress = CircuitBreaker::is_account_egress(from).is_some();
132134
let is_to_egress = CircuitBreaker::is_account_egress(to).is_some();
@@ -135,13 +137,10 @@ impl<RC: Get<AssetId>> orml_traits::currency::OnTransfer<AccountId, AssetId, Bal
135137
return Ok(());
136138
}
137139

138-
let try_convert = || WithdrawCircuitBreaker::<RC>::convert_to_hdx(asset_id, amount);
140+
let try_convert = || WithdrawCircuitBreaker::convert_to_hdx(asset_id, amount);
139141

140-
if WithdrawCircuitBreaker::<RC>::should_account_withdraw_operation(
141-
asset_id,
142-
EgressOperationKind::Transfer,
143-
Some(to),
144-
) {
142+
if WithdrawCircuitBreaker::should_account_withdraw_operation(asset_id, EgressOperationKind::Transfer, Some(to))
143+
{
145144
let amount_ref_currency = try_convert()?;
146145
pallet_circuit_breaker::Pallet::<Runtime>::note_egress(amount_ref_currency)?;
147146
}
@@ -151,7 +150,7 @@ impl<RC: Get<AssetId>> orml_traits::currency::OnTransfer<AccountId, AssetId, Bal
151150
// accounts are never ingress — no tokens arrived from another chain)
152151
if is_from_egress
153152
&& matches!(
154-
WithdrawCircuitBreaker::<RC>::global_asset_category(asset_id),
153+
WithdrawCircuitBreaker::global_asset_category(asset_id),
155154
Some(GlobalAssetCategory::Local)
156155
) {
157156
if let Ok(amount_ref_currency) = try_convert() {
@@ -162,8 +161,8 @@ impl<RC: Get<AssetId>> orml_traits::currency::OnTransfer<AccountId, AssetId, Bal
162161
}
163162
}
164163

165-
pub struct OnDepositHook<RC>(PhantomData<RC>);
166-
impl<RC: Get<AssetId>> orml_traits::Handler<(AssetId, Balance, Option<AccountId>)> for OnDepositHook<RC> {
164+
pub struct OnDepositHook;
165+
impl orml_traits::Handler<(AssetId, Balance, Option<AccountId>)> for OnDepositHook {
167166
fn handle(t: &(AssetId, Balance, Option<AccountId>)) -> DispatchResult {
168167
let (asset_id, amount, maybe_dest) = t;
169168

@@ -176,14 +175,14 @@ impl<RC: Get<AssetId>> orml_traits::Handler<(AssetId, Balance, Option<AccountId>
176175
// Outside XCM, use the stricter should_account_deposit_operation check which,
177176
// for Local assets, requires the source to be an egress account.
178177
if buffer_active {
179-
if WithdrawCircuitBreaker::<RC>::global_asset_category(*asset_id).is_none() {
178+
if WithdrawCircuitBreaker::global_asset_category(*asset_id).is_none() {
180179
return Ok(());
181180
}
182-
} else if !WithdrawCircuitBreaker::<RC>::should_account_deposit_operation(*asset_id, maybe_dest.clone()) {
181+
} else if !WithdrawCircuitBreaker::should_account_deposit_operation(*asset_id, maybe_dest.clone()) {
183182
return Ok(());
184183
}
185184

186-
let Ok(amount_ref_currency) = WithdrawCircuitBreaker::<RC>::convert_to_hdx(*asset_id, *amount) else {
185+
let Ok(amount_ref_currency) = WithdrawCircuitBreaker::convert_to_hdx(*asset_id, *amount) else {
187186
return Ok(());
188187
};
189188

runtime/hydradx/src/xcm.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -690,9 +690,8 @@ impl TransactAsset for LocalAssetTransactor {
690690
CurrencyIdConvert::convert(what.clone()),
691691
IsNativeConcrete::<AssetId, CurrencyIdConvert>::matches_fungible(what),
692692
) {
693-
crate::circuit_breaker::WithdrawCircuitBreaker::<NativeAssetId>::ensure_inbound_xcm_withdraw_can_proceed(
694-
asset_id,
695-
amount,
693+
crate::circuit_breaker::WithdrawCircuitBreaker::ensure_inbound_xcm_withdraw_can_proceed(
694+
asset_id, amount,
696695
)
697696
.map_err(|e| XcmError::FailedToTransactAsset(e.into()))?;
698697
}

0 commit comments

Comments
 (0)