-
Notifications
You must be signed in to change notification settings - Fork 43
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
RFC: Better fee handling #53
RFC: Better fee handling #53
Conversation
This pull request has been mentioned on Polkadot Forum. There might be relevant details there: https://forum.polkadot.network/t/xcm-rfc-better-fee-handling/6547/1 |
A PoC was done for Polkadot in paritytech/polkadot-sdk#3450. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I am supportive of this change 👍 looking forward to community feedback
Overpaying for fees might have an increase in trapped assets. | ||
Depending on the method used for trapping assets, this might result in too much storage used. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When coupling this with a good fee discovery mechanism (being discussed in paritytech/polkadot-sdk#690), this concern is diminished. At least from users acting in good faith. We still need to consider if/how this can be exploited in bad faith.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah current trap asset implementation is flawed paritytech/polkadot-sdk#901
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would make sense to not trap fees, or (fees + assets) if the amount is below a particular threshold. That way we prevent extra storage bloat.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of trapping the asset, can you automatically RefundSurplus
and DepositAsset
back to the account being executed from, to reduce the likelihood that someone would forget to execute those operations and slowly lose assets over time?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of trapping the asset, can you automatically
RefundSurplus
andDepositAsset
back to the account being executed from, to reduce the likelihood that someone would forget to execute those operations and slowly lose assets over time?
I thought about this once, but this assumes an explicit implementation more related to XCVM rather than XCM (for example, consider cases where more than one asset is being withdrawn, how is this automatically handled by the transaction format without enforcing an XCVM specific behaviour?).
I think that part of the design is okay as it is now.
That said, yeah, the assets trap design can be improved.
Co-authored-by: Adrian Catangiu <adrian@parity.io>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess RefundSurplus
will need a revisit as well
The assets can't be used for anything else during the entirety of the program. | ||
This is different from the current semantics of `BuyExecution`. | ||
|
||
If not all `Assets` in the `fees` register are used when the execution ends, then we trap them. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also don't like trapping asset. With the current model, we almost always needs a RefundSurplus
+ DepositAsset
at end of the XCM to avoid trapped asset. Can we do it better?
I see a few options:
- Make
PayFees
also takes another optional refund address and automatically refund unused fees to it at end of the execution - Have a version of
DepositAsset
that deposit assets from all the asset register including fees and holdings - A
SetTrapAssetDestination
instruction that automatically deposit trapped asset into an account, instead of actually trap it - No change. Just inform devs that it is recommended to always have
RefundSurplus
+DepositAsset
at end of the XCM to avoid trapped asset.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree it's not the best to always require RefundSurplus
+ DepositAsset
.
I like the idea of depositing to some account instead of trapping the assets. At least letting the user specify that behaviour.
For example, SetAssetClaimer
is already approved, which lets the user specify who can claim trapped assets.
We could have something similar that allows the user to specify they want to deposit leftover assets to a particular location instead of trapping them.
However, this would just reduce the two instructions (RefundSurplus
+ DepositAsset
) to one (something like SetLeftoverFeesDestination
).
Not sure how much more useful that is.
We could make the default behaviour be to deposit the leftover fees to the Location specified by origin, given that we expect them to always be a little bit more than needed. If the user wants to specify the location for said leftover fees (say, an account they own in the local system that's not the sovereign account), they can specify it.
I think this would change the least assumptions about XCM programs. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't properly thought it through, so I won't offer an opinion on what is best option, but I want to call out early that we should design this (and further changes) to be easily usable in multi-hops XCM programs.
E.g. going through remote reserve where the user might not have an account, or going over a bridge where delivery fees need to be paid at both intermediary bridge-hubs, etc.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One benefit of SetXXX
is that we call it at the beginning of the execution and it will be executed no matter what. On the other hand, the RefundSurplus
at end may not be skipped because errors. We do have instructions to handle errors but then it maybe a bit more complicated? Actually we don't really have a best practice about how to write error-proof XCM and someone need to spend a bit of time to think about all the situations and come up some good recommendations. e.g. always have SetAppendix
to execute RefundSurplus
+ DepositAsset
to ensure no trapped asset no matter what.
Some disadvantage of SetXXX
instructions is that the implementation will be slightly more complicated as there are more state to remember. But I think that's not a real issue.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the SetX
instruction would need a new register holding a Location to where you want to deposit leftover fees. I think it's a good idea to have it.
Also, I agree best practices are definitely needed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was never a fan of asset traps because it is not trivial to recover traps, etc. - I like having a field in PayFees
where assets left in the holding register are sent to if program execution fails.
Even by default, I think the address should be the sovereign account of the origin chain. Therefore, it is up to the governance system of such a chain to help recover the assets.
@xlc - you can always put the RefundSurplus
+ DepositAsset
within a SetAppendix
so that it is "ensured" that they get executed if program execution fails. That is what we do with the XCM-Transactor we use at Moonbeam to do remote transacts (although we make it optional because before the cost of adding those extra instructions was "high")
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was never a fan of asset traps because it is not trivial to recover traps, etc. - I like having a field in PayFees where assets left in the holding register are sent to if program execution fails.
With the upcoming SetAssetClaimer, you could achieve exactly that without having to "bake it in" the PayFees
instruction.
Redesigning the asset trapping mechanism is also something on the TODO list, with ideas like depositing directly to sovereign accounts (custom account if set by SetAssetClaimer
or the sovereign account of the origin chain by default). But that should be discussed in a dedicated RFC imo.
We had another XCM transfer issue on Acala related to Acala specific fee handling that could be addressed by this new fee mechanism by allowing the parachain to charge additional fees on top of the existing XCM fee types. |
AFAICT the current proposal would allow that if you're implementing custom executor with custom What kind of "additional fees" should we be thinking about? Is it something we should just add support for in existing executor (some customizable extra fee types)? |
The only config item we have for fees is the Trader, but that's tightly coupled to weight. We could provide a new config item to make it easier to define custom fees. That's regardless of the standard |
For the Acala specific use case, we charge an additional storage deposit for EVM calls. In the case that the EVM call is triggered by XCM, we found we are in a poor situation that we don't have a good way to charge this deposit. We cannot charge it from the sovereign account, because otherwise it will be an attack vector to drain those accounts. |
What I'm thinking we can do after this new The good approach I can think of is adding a config item that can be a tuple of structs that have access to the Just brainstorming here. The additional fees scenario is a different one from the main one this RFC is trying to solve though. |
that wouldn’t be enough. ERC20 tokens are first class citizens at Acala. ie we allow transfer them via XCM using deposit/withdraw mechanism. so it is possible to invoke EVM without transact We currently have a global variable (implemented using a storage entry) to provide additional context to EVM so it knows where to charge the storage deposit. A fee register if implemented properly, will allow us to charge deposit from it. And we can simply make deposit/withdraw action to fail if unable to charge enough deposits. |
The interface of the I'd like to discuss the usage of this instruction by clients/users. And it would be awesome to be able to do something like this:
To achieve this, we could introduce an abstract asset type that the XCM Executor could use internally (we can call them "XCM credits"). I'm not sure if it should be part of the spec; it seems more like an implementation detail. Still, I believe it is worth discussing here nonetheless. The "XCM credits" may be used to cover:
A concrete blockchain could define the appropriate two-way conversion between its acceptable payment assets and these Credits. So, the
The XCM Fee Payment Runtime API could also be modified accordingly so clients can:
|
While there are some use cases of accepting multiple tokens for fee payments, I feel it is an edge case that can be easily avoided on the frontend side and the additional complicity does not worth the trade-off. The frontend should just figure out what asset should be used for XCM fee and optionally provide a way for user to change that. If no single asset is enough, display not enough fee is a reasonable outcome. |
@xlc Even if we don't want to support payment in multiple user-supplied currencies, there is another reason to introduce abstract Credits (by the way, in the simplest case, they can be defined as equivalent to the chain's native token). I am worried that the execution fee and the delivery fee (and any potential custom fee types) may currently require different tokens for payment on a single chain. With the The idea is to introduce an abstract asset to express any fee type so any acceptable user-supplied asset (singular) will cover all the fee types on a single chain (any acceptable asset can be converted to Credits). Thus, the user or a JS client won't need to think about different fee types and different required assets for each one within a single chain. They would be able to select a singular token to cover everything. Payment in multiple currencies becomes possible with this approach (and the XCM executor can handle it without worrying the chain developers), but it is optional. |
I don’t think that’s a real issue as the xcm on different chain are different and therefore it is possible to specify what assets for fee payment on each chain. It will be silly for a chain to require multiple assets for fee payments. Besides, the credit system will make refund fee lot more complicated. |
I'm worried because AssetHub does this. You can pay for the execution using USDT, but you also must have enough DOT to cover the delivery fees. It is a small fee, but nonetheless -- you pay for the execution in USDT and delivery in DOT.
|
Going forward I hope to hear from the ecosystem about these kinds of problems and try and fix them instead of building complicated machinery either on core protocol or upper layers to handle them. What you describe is a good example of this: a problem we need to fix (paritytech/polkadot-sdk#3958) rather than build more complexity around it to work around. |
If taking several assets for different fee types on a single chain is always considered a problem that the chain's team should fix, I'm okay with that :) I'd just be happy to see an interface that makes it easy "to do fee-related things right" and harder "to do wrong". However, the credit system would be an unpleasant migration for chains with big XCM configs (such as Acala). Also, an audit of the XCM executor would certainly be required, even if the actual implementation is relatively simple. So, yeah, the credit system requires much stronger reasons to be introduced. Speaking of interface, if we agree that only one asset should be used to cover all the fees on a single chain, should the |
While the chain should only require a single asset for fee payment, the chain may also accept multiple assets for fee payment purpose. In the example of Acala, we accept multiple tokens for XCM fee payment including ACA, DOT, LDOT, etc. User only need to supply one of those accepted token and able to perform XCM execution on Acala. |
Of course. It is not what I'm asking. The pub struct Asset {
/// The overall asset identity (aka *class*, in the case of a non-fungible).
pub id: AssetId,
/// The fungibility of the asset, which contains either the amount (in the case of a fungible
/// asset) or the *instance ID*, the secondary asset identifier.
pub fun: Fungibility,
} At the same time, pub struct Assets(Vec<Asset>); The current If If we make |
I wanted to make it more generic, maybe you don't have enough of one asset and want to pay with a combination of assets. I agree though that this is too complicated for something that should be simple, like fees. I'll revert it to take only one asset. |
This pull request has been mentioned on Polkadot Forum. There might be relevant details there: https://forum.polkadot.network/t/2024-04-23-technical-fellowship-opendev-call/7592/1 |
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 |
what's the next step? |
The next steps are finishing up the prototype, getting more RFCs for v5 and creating v5 |
I've been busy with other tasks, but I can start with v5 in some capacity |
Closing in favor of polkadot-fellows/RFCs#105 |
# 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 go through a different mechanism altogether. The idea is to fix this leveraging the `AssetExchanger` config item that's able to turn the asset the user wants to pay fees in into the asset the router expects for delivery fees. # Main addition An adapter was needed to use `pallet-asset-conversion` for exchanging assets in XCM. This was created in #5130. The XCM executor was modified to use `AssetExchanger` (when available) to swap assets to pay for delivery fees. ## Limitations We can only pay for delivery fees in different assets in intermediate hops. We can't pay in different assets locally. The first hop will always need the native token of the chain (or whatever is specified in the `XcmRouter`). This is a byproduct of using the `BuyExecution` instruction to know which asset should be used for delivery fee payment. Since this instruction is not present when executing an XCM locally, we are left with this limitation. To illustrate this limitation, I'll show two scenarios. All chains involved have pools. ### Scenario 1 Parachain A --> Parachain B Here, parachain A can use any asset in a pool with its native asset to pay for local execution fees. However, as of now we can't use those for local delivery fees. This means transfers from A to B need some amount of A's native token to pay for delivery fees. ### Scenario 2 Parachain A --> Parachain C --> Parachain B Here, Parachain C's remote delivery fees can be paid with any asset in a pool with its native asset. This allows a reserve asset transfer between A and B with C as the reserve to only need A's native token at the starting hop. After that, it could all be pool assets. ## Future work The fact that delivery fees go through a totally different mechanism results in a lot of bugs and pain points. Unfortunately, this is not so easy to solve in a backwards compatible manner. Delivery fees will be integrated into the language in future XCM versions, following polkadot-fellows/xcm-format#53. Old PR: #4375.
Moved from polkadot-fellows/xcm-format#53. The idea is to extend the current XCM fee mechanism to support in principle delivery fees, but potentially other types of fees as well. This is accomplished by having two additions: - `PayFees` instruction - A `fees` register, where fees go and stay there until the end of execution. Crucially, separate from the `holding` register --------- Co-authored-by: Adrian Catangiu <adrian@parity.io>
# 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 go through a different mechanism altogether. The idea is to fix this leveraging the `AssetExchanger` config item that's able to turn the asset the user wants to pay fees in into the asset the router expects for delivery fees. # Main addition An adapter was needed to use `pallet-asset-conversion` for exchanging assets in XCM. This was created in #5130. The XCM executor was modified to use `AssetExchanger` (when available) to swap assets to pay for delivery fees. ## Limitations We can only pay for delivery fees in different assets in intermediate hops. We can't pay in different assets locally. The first hop will always need the native token of the chain (or whatever is specified in the `XcmRouter`). This is a byproduct of using the `BuyExecution` instruction to know which asset should be used for delivery fee payment. Since this instruction is not present when executing an XCM locally, we are left with this limitation. To illustrate this limitation, I'll show two scenarios. All chains involved have pools. ### Scenario 1 Parachain A --> Parachain B Here, parachain A can use any asset in a pool with its native asset to pay for local execution fees. However, as of now we can't use those for local delivery fees. This means transfers from A to B need some amount of A's native token to pay for delivery fees. ### Scenario 2 Parachain A --> Parachain C --> Parachain B Here, Parachain C's remote delivery fees can be paid with any asset in a pool with its native asset. This allows a reserve asset transfer between A and B with C as the reserve to only need A's native token at the starting hop. After that, it could all be pool assets. ## Future work The fact that delivery fees go through a totally different mechanism results in a lot of bugs and pain points. Unfortunately, this is not so easy to solve in a backwards compatible manner. Delivery fees will be integrated into the language in future XCM versions, following polkadot-fellows/xcm-format#53. Old PR: #4375.
XCM already handles execution fees in an effective and efficient manner using the
BuyExecution
instruction.However, other types of fees are not handled as effectively -- for example, delivery fees.
Fees exist that can't be measured using
Weight
-- as execution fees can -- so a new method should be thought up for those cases.This RFC proposes making the fee handling system simpler and more general, by doing two things:
fees
registerBuyExecution
with a new instructionPayFees
with new semanticsThis new instruction only takes the amount of fees that the XCVM can use from the holding register.
The XCVM implementation is free to use these fees to pay for execution fees, transport fees, or any other type of fee that might be necessary.
This moves the specifics of fees further away from the XCM standard, and more into the actual underlying XCVM implementation, which is a good thing.