Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support different assets for delivery fees #4375

Draft
wants to merge 12 commits into
base: master
Choose a base branch
from

Conversation

franciscoaguirre
Copy link
Contributor

@franciscoaguirre franciscoaguirre commented May 3, 2024

Context

Fees can already be paid in other assets locally thanks to the Trader implementations we have.
This doesn't work when sending messages because delivery fees can only be paid in one asset.
The idea is to fix this by providing an AssetConverter that's able to turn the asset the user wants to pay fees in into the asset the router expects for delivery fees.

Paying delivery fees in USDT instead of WND on Westend Asset Hub (uses min_balance ratio since it's sufficient):
Screenshot 2024-05-03 at 20 32 24

Paying delivery fees in a custom asset in a pool with WND (notice how the custom asset is withdrawn but WND is deposited):
Screenshot 2024-05-21 at 11 37 43

Main addition

A new xcm executor config item is introduced: AssetConverter, that can convert balances from one asset to another.
It's used for paying delivery fees in different assets other than the native one (the one specified in the XcmRouter).

How it works

In order to pay delivery fees with different assets, you need to add some type as the AssetConverter in your XCM executor.
Right now, the AssetConverter approach will only work for delivery fees, for execution fees, you need to copy the same logic onto a Trader.
The logic is mostly the same, only the interface changes. The Trader has to do a little bit more work.
This duplicated logic is planned to be removed in v5, when we have the same mechanism for both execution and delivery fees.

If there is no AssetConverter then nothing will break as long as you pay fees in the intended asset (the one in XcmRouter).
If there is, then you can define which assets can be used for fees.

Tests

The following asset hub westend integration tests show scenarios where this is used:

  • Sufficient trust backed assets
    • Reserve asset transfer from system para to para
    • Reserve asset transfer from para to para through system para
  • Sufficient foreign assets
    • Reserve asset transfer from system para to para
    • Reserve asset transfer from para to para through system para
  • Pool assets
    • Reserve asset transfer from system para to para
    • Reserve asset transfer from para to para through system para

Breaking changes

This PR breaks the following things:

  • New AssetConverter needs to be set in the executor config, even if it's just ().
  • XcmExecutor::charge_fees signature changed.
  • Pending changes to the executor to allow setting the desired asset to pay for fees.

Dear reviewer

TODO

@muharem muharem self-requested a review May 8, 2024 11:34
@Polkadot-Forum
Copy link

This pull request has been mentioned on Polkadot Forum. There might be relevant details there:

https://forum.polkadot.network/t/xcm-user-and-developer-experience-improvements/4511/21

@command-bot
Copy link

command-bot bot commented May 22, 2024

Here's a link to docs

Copy link
Contributor

@muharem muharem left a comment

Choose a reason for hiding this comment

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

What if we disable the FeeManager for our runtime setups and try to account a transport weights in Weigher. Even if we cannot do it properly, may be some reasonable constants would be enough.
For local execution the transport fee will be included in extrinsics' weights, and for the remote execution a user will pay with BuyExecution for transport fee as well.
To avoid a double payment with jit_withdraw fee mode, we will need to disable it via config (here we also can provide some info for dependent teams).

After reading the weights payment model in the xcm pallet and the executor, I do not see a good way to make it work with various assets through the FeeManager without breaking the APIs. With the new xcm version we can have a better solution.

},
Err(error) => {
// TODO: log::error is too noisy and this might get called a lot.
log::error!(
Copy link
Contributor

Choose a reason for hiding this comment

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

it's not really an error since we still trying

let actual_asset_to_use_for_fees = match Config::AssetConverter::convert_asset(&first, asset_id) {
Ok(new_asset) => new_asset,
Err(error) => {
log::error!(target: "xcm::DepositReserveAsset", "What happened?");
Copy link
Contributor

Choose a reason for hiding this comment

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

this will error every time when we had no asset_for_fees in registry

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah this case hasn't been done yet

Comment on lines 283 to 284
Config::AssetTransactor::withdraw_asset(&actual_asset_to_use, &origin, None)?;
Config::FeeManager::handle_fee(fees, None, FeeReason::ChargeFees);
Copy link
Contributor

Choose a reason for hiding this comment

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

this looks dangerous, to withdraw only one and handle all

@@ -2422,7 +2422,7 @@ impl<T: Config> Pallet<T> {
log::debug!(target: "xcm::send_xcm", "dest: {:?}, message: {:?}", &dest, &message);
let (ticket, price) = validate_send::<T::XcmRouter>(dest, message)?;
if let Some(fee_payer) = maybe_fee_payer {
Self::charge_fees(fee_payer, price).map_err(|_| SendError::Fees)?;
Self::charge_fees(fee_payer, price, &AssetId(Here.into())).map_err(|_| SendError::Fees)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

why is this always here? even if we want it to be native always, it wont be here for system parachains

weight_limit,
)?;
Self::execute_xcm_transfer(origin, dest, local_xcm, remote_xcm)
Self::execute_xcm_transfer(origin, dest, local_xcm, remote_xcm, fees.id)
Copy link
Contributor

Choose a reason for hiding this comment

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

the assumption is that a user always wanna pay local fees with the same asset as on a remote chain. this wont work for example when a user does a transfer all for fees.id asset.

fn charge_fees(location: Location, assets: Assets) -> DispatchResult {
T::XcmExecutor::charge_fees(location.clone(), assets.clone())
fn charge_fees(location: Location, assets: Assets, asset_for_fees: &AssetId) -> DispatchResult {
T::XcmExecutor::charge_fees(location.clone(), assets.clone(), asset_for_fees)
Copy link
Contributor

Choose a reason for hiding this comment

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

we could convert an asset from assets into asset_for_fees here, and avoid breaking the trait api at least for this case.

Copy link
Contributor

Choose a reason for hiding this comment

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

here I mean on the pallet

@@ -945,6 +1070,9 @@ impl<Config: config::Config> XcmExecutor<Config> {
// should be executed.
let Some(weight) = Option::<Weight>::from(weight_limit) else { return Ok(()) };
let old_holding = self.holding.clone();
// Store the asset being used for fees, so delivery fees can access it.
self.asset_for_fees = Some(fees.id.clone());
Copy link
Contributor

Choose a reason for hiding this comment

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

I think this is reasonable assumption that a user is fine to pay a transport fee with the same asset used for execution fee. The only problem is that local executions wont have the BuyExecution instruction.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's my main concern, and the reason why I did the "looping through all assets in holding" trick, which I know is bad.
If local executions had a BuyExecution instruction then this would be great.
My current thought is to create a new entrypoint prepare_and_execute_with_fee_asset.

self.origin_ref(),
self.fees_mode,
reason,
);
let first = fees.get(0).ok_or(XcmError::AssetNotFound)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

valid for our implementations, but if someone have more than one here, there is a problem

// It's not empty because of jit withdrawal, it's empty because it's taken from holding before sending.
// That's the problem jit withdrawal was solving, we need to "park" delivery fees in the same way we did
// for `DepositReserveAsset`.
for asset in self.holding.fungible_assets_iter() {
Copy link
Contributor

Choose a reason for hiding this comment

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

This way we basically deciding for a user which asset to use for the payment, this can be a surprise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, but some API needs to change or I have to add a new one to not have to do this.
I was thinking of creating a new entrypoint prepare_and_execute_with_fee_asset that pallet-xcm can use.
That'd keep the API the same for all other teams.
What do you think?

@paritytech-cicd-pr
Copy link

The CI pipeline was cancelled due to failure one of the required jobs.
Job name: cargo-clippy
Logs: https://gitlab.parity.io/parity/mirrors/polkadot-sdk/-/jobs/6263079

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T6-XCM This PR/Issue is related to XCM.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

5 participants