Skip to content
Open
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
46 changes: 46 additions & 0 deletions integration-tests/src/global_withdraw_limit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1528,3 +1528,49 @@ fn inbound_xcm_incomplete_message_still_accounts_egress() {
);
});
}

#[test]
fn polkadot_xcm_execute_withdraw_external_asset_succeeds_when_oracle_cannot_price_route() {
TestNet::reset();
Hydra::execute_with(|| {
init_global_withdraw_limit_params();

assert_ok!(CircuitBreaker::set_asset_category(
hydradx_runtime::RuntimeOrigin::root(),
DOT,
Some(GlobalAssetCategory::External)
));
// XCM asset transactor needs a location -> asset id mapping for DOT.
assert_ok!(hydradx_runtime::AssetRegistry::set_location(DOT, DOT_ASSET_LOCATION));
assert_ok!(hydradx_runtime::MultiTransactionPayment::add_currency(
hydradx_runtime::RuntimeOrigin::root(),
DOT,
FixedU128::from_rational(50, 100),
));
pallet_transaction_multi_payment::AcceptedCurrencyPrice::<hydradx_runtime::Runtime>::insert(
DOT,
FixedU128::from_rational(50, 100),
);

let alice: AccountId = ALICE.into();
let amount = 10 * UNITS;
let alice_dot_before = Currencies::free_balance(DOT, &alice);
assert!(alice_dot_before >= amount);

//Act
let message = xcm_message_withdraw_deposit(Location::parent(), amount);
let call = RuntimeCall::PolkadotXcm(pallet_xcm::Call::execute {
message: Box::new(VersionedXcm::from(message)),
max_weight: Weight::from_parts(1_000_000_000_000, 0),
});

assert_ok!(call.dispatch(hydradx_runtime::RuntimeOrigin::signed(ALICE.into())));

//Assert
assert!(
Currencies::free_balance(DOT, &alice) <= alice_dot_before - amount,
"DOT must have been withdrawn from Alice for the outbound XCM; before={alice_dot_before}, after={}",
Currencies::free_balance(DOT, &alice)
);
});
}
14 changes: 9 additions & 5 deletions runtime/hydradx/src/circuit_breaker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ use pallet_asset_registry::AssetType;
use pallet_circuit_breaker::types::EgressOperationKind;
use pallet_circuit_breaker::GlobalAssetCategory;
use primitives::Balance;
use sp_runtime::helpers_128bit::multiply_by_rational_with_rounding;
use sp_runtime::traits::Convert;
use sp_runtime::DispatchResult;
use sp_runtime::{DispatchResult, FixedPointNumber, FixedU128, Rounding};
use sp_std::marker::PhantomData;

pub struct WithdrawLimitHandler<RC>(PhantomData<RC>);
Expand All @@ -26,14 +27,17 @@ impl<ReferenceCurrencyId: Get<AssetId>> WithdrawCircuitBreaker<ReferenceCurrency
return Ok(amount);
}

let (converted, _) = ConvertBalance::<TenMinutesOraclePrice, XykPaymentAssetSupport, DotAssetId>::convert((
ConvertBalance::<TenMinutesOraclePrice, XykPaymentAssetSupport, DotAssetId>::convert((
Copy link
Copy Markdown
Contributor

@F3Joule F3Joule Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

convert_to_hdx is generic over ReferenceCurrencyId, but the fallback assumes ref currency = HDX (since currency_price always returns price relative to the native asset). Current callers all pass NativeAssetId so this is fine, but consider a debug_assert! or comment to guard against future misuse.

ConvertBalance::<TenMinutesOraclePrice, XykPaymentAssetSupport, DotAssetId>::convert((

asset_id,
ref_currency,
amount,
))
.ok_or(pallet_circuit_breaker::Error::<Runtime>::FailedToConvertAsset)?;

Ok(converted)
.map(|(converted, _)| converted)
.or_else(|| {
let price = MultiTransactionPayment::currency_price(asset_id)?;
multiply_by_rational_with_rounding(amount, FixedU128::DIV, price.into_inner(), Rounding::Up)
})
.ok_or_else(|| pallet_circuit_breaker::Error::<Runtime>::FailedToConvertAsset.into())
}

pub fn global_asset_category(asset_id: AssetId) -> Option<GlobalAssetCategory> {
Expand Down
Loading