Extend pallet-xcm to pay teleport with non teleported asset#266
Extend pallet-xcm to pay teleport with non teleported asset#266
Conversation
…target_dir=trappist
pallets/proxy-teleport/src/lib.rs
Outdated
| dest: Box<VersionedMultiLocation>, | ||
| beneficiary: Box<VersionedMultiLocation>, | ||
| native_asset_amount: u128, | ||
| proxy_asset: Box<VersionedMultiAssets>, |
There was a problem hiding this comment.
is this input parameter actually required?
My understanding is that it is always going to be the Relay chain/system parachain native asset hence the user would always set the same input.
There was a problem hiding this comment.
Hey, I see your point, I left it as a parameter as I thought that there might be an use case in which you would like to buy execution with other asset that is handeld but is not the Relay/SP native. For example buying with execution with xUSD on AH. What do you think?
There was a problem hiding this comment.
good point, I agree it makes sense to leave this flexibility for the future. Only thing, it would be good to document this, particularly that the sovereign account in AH will need to have enough balance of the relevant asset for this to work (apart from it having a liquidity pool vs the native asset etc)
pallets/proxy-teleport/src/lib.rs
Outdated
| //Build the message to execute on origin. | ||
| let assets: MultiAssets = assets.into(); | ||
| let message: Xcm<<T as frame_system::Config>::RuntimeCall> = Xcm(vec![ | ||
| // Withdraw drops asset so is used as burn mechanism |
There was a problem hiding this comment.
I was wondering, is it possible to mimic what InitiateTeleport does so that we don't leave the native asset trapped in the holding?
There was a problem hiding this comment.
This way we could directly pass assets in line 172 instead of the hardcoded foreing_assets.
There was a problem hiding this comment.
I like this, on it! Thanks
There was a problem hiding this comment.
Update:
- I have implemented the
reanchor,foreign_assetsis no longer hardcoded. - I have also added a
BurnAssetsto mimic this part of theInitiateTeleportinstruction - Currently trying to solve this
check_outlogic for this proxy call to track teleports too.
There was a problem hiding this comment.
Good idea to use BurnAsssets instruction. In my opinion this is a solution close to as clean as we can get it. This way there is no trapped assets in the holding whereas the counterpart is expected to be minted in the destination chain. I think the check out logic is then secondary.
IkerAlus
left a comment
There was a problem hiding this comment.
I left some general comments around the new pallet for the proxy teleport, great work!
pallets/proxy-teleport/src/lib.rs
Outdated
| let xcm_message: Xcm<()> = Xcm(vec![ | ||
| WithdrawAsset(proxy_asset), | ||
| BuyExecution { fees, weight_limit }, | ||
| ReceiveTeleportedAsset(foreing_assets.clone()), |
There was a problem hiding this comment.
I don't get the need for the ReceiveTeleportedAsset instruction .. can you explain ?
There was a problem hiding this comment.
Hey! Sure.
We must take in consideration that:
InitiateTeleportinstruction imprints aReceiveTeleportedAssetas first instruction of the xcm message that it sends.- The
Barrierthat we are targeting for in Asset Hub isAllowTopLevelPaidExecutionFrom, this Barrier to pass imposes a certain sequence of Instructions to be received in order as detailed here. - That is why
do_teleport_assetsfn frompallet-xcmsetsBuyExecutionas the first instruction of the xcm message that is sent as parameter toInitiateTeleport, therefore the Instruction order to be receive would be:
--Receive->ClearOrigin(fromInitiateTeleport)
--BuyExecution(from the xcm sent as parameter ofInitiateTeleport)
Leading to the Barrier compliant sequence ofReceiveTele...->ClearOrigin->BuyExecution
However, on Asset Hub we cannot BuyExecution with HOP or teleport ROC into it which would trigger the ReceiveTeleportedAssets progression with ROC.
There is no Instruction that allows me to prepend a Withdraw (ROC) + BuyExecution (ROC) together with the InitiateTeleport logic.
Therefore I am forced to split the logic of the call in two parts: What should be executed on Trappist and what should be sent to be executed on Asset Hub. I am trying to handcraft the logic as close as possible to the one from InitiateTeleport but adding the extra instructions that are needed for the use case. This leads to:
- A
execute_xcmfor taking the assets to be teleported on origin viaWithdraw+Burn - A
send_xcmfor paying execution with ROC and apply the "teleport" logic on Asset Hub. This is done by theWithdraw+BuyExecution+ReceiveTele...+DepositAssetthat is sent to be executed on Asset Hub.
With these Instructions on this combination I try to approach to the do_teleport_assets logic but sneaking an execution buy with ROC on AH.
I hope this explains both this approach and the rest of the comments, please do let me know if you have any further question or if something doesn't make sense since this is just how I figured out how to work around this issue
pallets/proxy-teleport/src/lib.rs
Outdated
| let message: Xcm<<T as frame_system::Config>::RuntimeCall> = Xcm(vec![ | ||
| // Withdraw drops asset so is used as burn mechanism | ||
| WithdrawAsset(assets.clone()), | ||
| BurnAsset(assets), |
There was a problem hiding this comment.
Indeed, shouldn't it be a InitiateTeleport instruction instead of the BurnAsset one ?
pallets/proxy-teleport/src/lib.rs
Outdated
| root_origin.try_into().map_err(|_| pallet_xcm::Error::<T>::InvalidOrigin)?; | ||
| //TODO: Check this Error population | ||
| let message_id = BaseXcm::<T>::send_xcm(interior, dest, xcm_message.clone()) | ||
| .map_err(|_| Error::<T>::SendError)?; |
There was a problem hiding this comment.
Shouldn't you use execute_xcm instead of send_xcm, to initiate the teleport port operation on the local chain ?
pallets/proxy-teleport/src/lib.rs
Outdated
| //Unbox proxy asset | ||
| let proxy_asset: MultiAssets = | ||
| (*proxy_asset).try_into().map_err(|()| pallet_xcm::Error::<T>::BadVersion)?; |
There was a problem hiding this comment.
You might want to unbox this earlier. You're gonna use it anyway and reanchoring is more expensive than this conversion.
There was a problem hiding this comment.
Done but would you care to explain further ?
There was a problem hiding this comment.
You don't want to perform an expensive operation only to then find out your boxed value had a bad version
There was a problem hiding this comment.
Understood and makes sense, feel free to resolve.
| Self::deposit_event(Event::Attempted { outcome }); | ||
|
|
||
| // Use pallet-xcm send for sending message. | ||
| let root_origin = T::SendXcmOrigin::ensure_origin(frame_system::RawOrigin::Root.into())?; |
There was a problem hiding this comment.
The origin should be the account that has the funds it wants to teleport
There was a problem hiding this comment.
I agree that the one paying for the execution with the proxy asset on destiny chain should be the SovereignAccount (SA from now on) of the sender account and not the SA of the chain.
As a quick recap, proxy asset indicates which asset should be used to buy execution on destiny, but the funding of the account that is going to pay for that execution with said asset must be done previously to this call.
For simplicity, let's focus on the use case of: origin chain is Trappist, destiny chain is Asset Hub and the proxy asset is ROC.
Funding the SA of Trappist is simple, you can obtain the SA address with this tool and the PARA ID. Then you do a simple transfer of ROC on Asset Hub to the address of Trappist's SA. Therefore why I currently set Root as origin.
However doing so for the SA of the caller origin account from Trappist in Asset Hub, let's call it Alice, is not straightforward. Two good solutions would be:
- Creating a tool that exposes this to obtain the address given a certain MultiLocation and send ROC to it on AH.
- As you suggested on our related talk, another approach would be doing a XCM
TransferAssetof ROC to{1, X2(Trappist,Alice)}which is the SA of Trappist's Alice on AH (or anySibling). However, atm, AH doesn't allow executions.
Moving forward, I implemented the HashedDescription on Trappist's LocationToAccountId and was able to get the address SA of Trappist's Alice, meaning the SA of {1, X2(Trappist,Alice)} MultiLocation.
This allowed me to send ROC on AH to this address and now the call buys execution successfully if we set the origin of the call to Alice instead of Root. However, {1, X2(Trappist,Alice)} is not considered a Trusted Teleporter by AH.
Using an alias of Alice's SA to Trappist's SA which is currently a TrustedTeleported would be a increment of privileges on Alice that I do not see fit and also AH doesn't accept any Aliasers.
There was a problem hiding this comment.
I don't think the name "proxy asset" is the best to describe what's going on here. The idea of this pallet, and extrinsic, is to teleport one main asset to chain B but withdraw another one on B as well.
It might be called something like withdraw_and_teleport.
It's just the name "proxy" the one that seems misused, it could be called a "fee asset" and that might be better.
There was a problem hiding this comment.
Regarding using Alice as the origin of the message, in the end, it's not needed, Alice will already pay up on the sender chain, in this case Trappist, with derivative assets, in this case derivative ROCs.
It's perfectly fine to Withdraw from Trappist's sovereign account since AH doesn't care about Trappist's internal bookkeeping, it only cares that the whole of Trappist has some amount of ROCs. InitiateReserveWithdraw handles it in that way.
On another note, if you only fund Trappist's sovereign account on AH, you're basically giving up power over those assets and are unable to use them again. You should instead use DepositReserveAsset. That will fund the SA but also send a message over to Trappist to mint derivative tokens and send them to Alice over there. That will allow Alice to use this extrinsic to teleport whatever she wants, along with withdrawing the real ROCs for fees.
There was a problem hiding this comment.
Agreed on the wording, as logic evolved the proxy term that I initially used became outdated, will re work.
There was a problem hiding this comment.
As for the DepositReserveAsset, will explore the suggestion as it would solve two main points:
- Users or chain admins having to fund the SA with a
transferwhich requires them to obtain the address of the SA and adds an extra step of friction. - Most importantly, it diminishes vulnerabilities as it would take any incentive to drain the SA because users would be required to have the derivative on origin chain, and to do so they would have needed to reserve them, so basically it would be their funds.
As discussed on our following chat regarding this comment, withdrawn ROC derivatives on Trappist should also be burned and surplus of ROCs on Asset Hub could be refunded to avoid trapping unnecessary users funds.
From usability perspective, users would no longer need to fund SA through a transfer and they would need to ReserveAssetTransfer ROCs to their addresses on Trappist to be able to do this teleport.
Will implement and update on this thread.
pallets/proxy-teleport/src/lib.rs
Outdated
| WithdrawAsset(proxy_asset), | ||
| BuyExecution { fees, weight_limit }, | ||
| ReceiveTeleportedAsset(foreing_assets.clone()), | ||
| // Intentionally trap ROC to avoid exploit |
There was a problem hiding this comment.
I don't see why keeping this ROC around would be a potential exploit. Care to explain?
There was a problem hiding this comment.
Yet to be explored but initially I was depositing All assets on beneficiary, and since at the time ROC is coming from pre funded Trappist's SovereignAccount on Asset Hub, there could be a case in which multiple calls drain the funds from the SovereignAccount into the beneficiary.
Limiting the amount of the proxy_asset, or demanding at least certain amount of the native_asset to be teleported could be a starter to avoid this. Atm, by not depositing all assets and only the foreign/native asset whose equivalent amount has been withdrawn on origin, users couldn't benefit from this exploit. However they could drain the funds by trapping all of them because some people just want to watch the world burn.
Changing origin to the from root to the sender would totally fix this as the funds of ROC (or any proxy_asset) would have been already owned by the caller.
There was a problem hiding this comment.
I explained the way things work on my other comment. The funds are always coming from a user, since they pay on the sender chain (via burning). The only potential exploit there is minting more than is burnt. If the mechanism works well, Trappist's SA should never be arbitrarily drained, it will always be the result of derivative tokens being burnt.
Trappist's SA's balance in ROCs = sum(balance in derivative ROC for account in Trappist) <- If this holds then it all works fine.
There was a problem hiding this comment.
Agreed that, if implemented, would solve this existing vulnerability.
pallets/withdraw-teleport/src/lib.rs
Outdated
| WithdrawAsset(fee_asset), | ||
| BuyExecution { fees, weight_limit }, | ||
| ReceiveTeleportedAsset(foreing_assets.clone()), | ||
| // Intentionally trap ROC to avoid exploit of draining Sovereing Account |
There was a problem hiding this comment.
Doesn't look trapped?
IMO this is hacky. Just debit the user of ROC here.
There was a problem hiding this comment.
I would need some more explanation here as I do not fully understand where are you suggesting to debit the ROC from. Thanks!
There was a problem hiding this comment.
I mean here on Trappist. You have reserve-backed ROC here, right?
There was a problem hiding this comment.
No initially, but yes after #275 , so shall we move forward and merge 275 which solves this ?
pallets/withdraw-teleport/src/lib.rs
Outdated
| // aware of this and implement a mechanism to prevent draining. | ||
| WithdrawAsset(fee_asset), | ||
| BuyExecution { fees, weight_limit }, | ||
| ReceiveTeleportedAsset(foreing_assets.clone()), |
There was a problem hiding this comment.
| ReceiveTeleportedAsset(foreing_assets.clone()), | |
| ReceiveTeleportedAsset(foreing_assets.clone()), | |
| RefundSurplus, |
There was a problem hiding this comment.
I agree but in this case I didn't initially intend to refund used ROCs to avoid this exploit.
TL:DR since users would be calling this extrinsic from Trappist and the execution in Asset Hub is bought with ROC coming from Trappist's AH SovereignAccount, refunding the ROC surplus could be used to drain the SovAcc.
A solution that is based on Cisco and Iker's input is presented here but implies users reserve transferring ROC into their Trappist's account.
There was a problem hiding this comment.
Yeah I see, but like 275 shows can't you burn the ROC here and then transfer the amount out (partly for fee payment and partly to beneficiary) on AH?
There was a problem hiding this comment.
Indeed, so I think that #275 is establishing as the way to go.
pallets/withdraw-teleport/src/lib.rs
Outdated
| ReceiveTeleportedAsset(foreing_assets.clone()), | ||
| // Intentionally trap ROC to avoid exploit of draining Sovereing Account | ||
| // by depositing withdrawn ROC on beneficiary. | ||
| DepositAsset { assets: MultiAssetFilter::Definite(foreing_assets), beneficiary }, |
There was a problem hiding this comment.
Why not just AllOf whatever is left?
There was a problem hiding this comment.
Also, this is depositing the Trappist token, but the comment says (and I believe is correct) that it should deposit ROC.
There was a problem hiding this comment.
I think this is what you want.
| DepositAsset { assets: MultiAssetFilter::Definite(foreing_assets), beneficiary }, | |
| DepositAsset { assets: AllCounted(2).into(), beneficiary } |
There was a problem hiding this comment.
Agreed and initially did it that way but changed to depositing only teleported foreign_assets because of this.
Co-authored-by: Steve Degosserie <steve@parity.io> Co-authored-by: joe petrowski <25483142+joepetrowski@users.noreply.github.com>
|
LGTM ! Let's open additional PRs to add proper benchmarks, and tests (which most likely requires the integration of xcm-emulator instead of the current xcm-simulator). |
Intro
First approach to an extension for pallet-xcm to allow a native asset Teleport from Trappist (or any para) into Asset Hub.
Issue
Our main issue at the moment is that even though we are able to register HOP (Trappist native asset) as a foreign asset on Asset Hub, we cannot teleport it into AH. This is related to how
InitiateTeleportcall of XCM works, which is the one that also pallet-xcm uses for its limited and unlimited teleports. Said instruction triggers aReceiveTeleportedAssetson destiny and this “minted” assets are used for buying execution on destiny, however, AH doesn’t allow (yet) to buy execution with foreign assets and doesn’t recognize sibling parachains as ROC teleporters so we cannot send ROC on theAssetsvector of theTeleportto pay the fees. A further description can be found here.Workaround
We presented a workaround with batching multiple instruction calls through xcm pallet, this can also be found here but the main logic is:
Proposed Solution
I see that an abstraction of this process that might become useful also for other use cases is to create a call into a pallet-xcm extension that allows users to define an asset that is not being teleported to pay for execution in destiny This implies that the account that buys execution on destiny must be previously funded with said non-teleported asset, in this case we would be funding the account on AH with ROC before sending this call.
This first iteration presents a simple approach in which we only allow user to teleport native tokens out.
Disclaimer
I prioritized speed to start receiver feedback on the logic itself as soon as possible so there are some consciously
TODOs, for example weights and weight limit.Same goes for implementation, so please, do drop any suggestions.
I am particularly interested on your feedback on how to define the account to withdraw ROC from on AH, on this case I am using
Rootto force withdraw from Sovereign Account but this does introduce evident vulnerabilities.We can add
ensure rootto request sudo and turn it into a privileged call to get foreign assets into AH and from there distribute or fund pools but really eager to get your thoughts on this.Thanks.