Skip to content
This repository has been archived by the owner on May 21, 2024. It is now read-only.

Extend pallet-xcm to pay teleport with non teleported asset #266

Merged
merged 30 commits into from
Sep 15, 2023

Conversation

metricaez
Copy link
Collaborator

@metricaez metricaez commented Aug 24, 2023

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 InitiateTeleport call of XCM works, which is the one that also pallet-xcm uses for its limited and unlimited teleports. Said instruction triggers a ReceiveTeleportedAssets on 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 the Assets vector of the Teleport to 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:

  1. Burn HOP on Trappist
  2. Withdraw ROC from an already funded account on AH
  3. Buy execution with ROC
  4. ReceiveTeleported (mint) HOP on AH
  5. Deposit to Beneficiary

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 Root to force withdraw from Sovereign Account but this does introduce evident vulnerabilities.

We can add ensure root to 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.

@metricaez metricaez marked this pull request as draft August 24, 2023 21:20
@metricaez metricaez requested a review from IkerAlus August 24, 2023 21:20
dest: Box<VersionedMultiLocation>,
beneficiary: Box<VersionedMultiLocation>,
native_asset_amount: u128,
proxy_asset: Box<VersionedMultiAssets>,

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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?

Choose a reason for hiding this comment

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

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)

//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

Choose a reason for hiding this comment

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

I was wondering, is it possible to mimic what InitiateTeleport does so that we don't leave the native asset trapped in the holding?

Choose a reason for hiding this comment

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

This way we could directly pass assets in line 172 instead of the hardcoded foreing_assets.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I like this, on it! Thanks

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Update:

  • I have implemented the reanchor , foreign_assets is no longer hardcoded.
  • I have also added a BurnAssets to mimic this part of the InitiateTeleport instruction
  • Currently trying to solve this check_out logic for this proxy call to track teleports too.

Copy link

@IkerAlus IkerAlus Aug 29, 2023

Choose a reason for hiding this comment

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

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.

Copy link

@IkerAlus IkerAlus left a comment

Choose a reason for hiding this comment

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

I left some general comments around the new pallet for the proxy teleport, great work!

let xcm_message: Xcm<()> = Xcm(vec![
WithdrawAsset(proxy_asset),
BuyExecution { fees, weight_limit },
ReceiveTeleportedAsset(foreing_assets.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 don't get the need for the ReceiveTeleportedAsset instruction .. can you explain ?

Copy link
Collaborator Author

@metricaez metricaez Aug 28, 2023

Choose a reason for hiding this comment

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

Hey! Sure.

We must take in consideration that:

  • InitiateTeleport instruction imprints a ReceiveTeleportedAsset as first instruction of the xcm message that it sends.
  • The Barrier that we are targeting for in Asset Hub is AllowTopLevelPaidExecutionFrom, this Barrier to pass imposes a certain sequence of Instructions to be received in order as detailed here.
  • That is why do_teleport_assets fn from pallet-xcm sets BuyExecution as the first instruction of the xcm message that is sent as parameter to InitiateTeleport, therefore the Instruction order to be receive would be:
    -- Receive -> ClearOrigin (from InitiateTeleport)
    -- BuyExecution (from the xcm sent as parameter of InitiateTeleport)
    Leading to the Barrier compliant sequence of ReceiveTele... -> 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_xcm for taking the assets to be teleported on origin via Withdraw + Burn
  • A send_xcm for paying execution with ROC and apply the "teleport" logic on Asset Hub. This is done by the Withdraw + BuyExecution + ReceiveTele... + DepositAsset that 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

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),
Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, shouldn't it be a InitiateTeleport instruction instead of the BurnAsset one ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed here

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)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't you use execute_xcm instead of send_xcm, to initiate the teleport port operation on the local chain ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Addressed here

pallets/proxy-teleport/src/lib.rs Outdated Show resolved Hide resolved
Comment on lines 143 to 145
//Unbox proxy asset
let proxy_asset: MultiAssets =
(*proxy_asset).try_into().map_err(|()| pallet_xcm::Error::<T>::BadVersion)?;
Copy link

@franciscoaguirre franciscoaguirre Sep 1, 2023

Choose a reason for hiding this comment

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

You might want to unbox this earlier. You're gonna use it anyway and reanchoring is more expensive than this conversion.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done but would you care to explain further ?

Choose a reason for hiding this comment

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

You don't want to perform an expensive operation only to then find out your boxed value had a bad version

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Understood and makes sense, feel free to resolve.

pallets/proxy-teleport/src/lib.rs Outdated Show resolved Hide resolved
pallets/proxy-teleport/src/lib.rs Outdated Show resolved Hide resolved
pallets/proxy-teleport/src/lib.rs Outdated Show resolved Hide resolved
pallets/proxy-teleport/src/lib.rs Outdated Show resolved Hide resolved
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())?;

Choose a reason for hiding this comment

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

The origin should be the account that has the funds it wants to teleport

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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 TransferAsset of ROC to {1, X2(Trappist,Alice)} which is the SA of Trappist's Alice on AH (or any Sibling). 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.

Choose a reason for hiding this comment

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

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.

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed on the wording, as logic evolved the proxy term that I initially used became outdated, will re work.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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 transfer which 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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

#275 Shows a first approach.

WithdrawAsset(proxy_asset),
BuyExecution { fees, weight_limit },
ReceiveTeleportedAsset(foreing_assets.clone()),
// Intentionally trap ROC to avoid exploit

Choose a reason for hiding this comment

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

I don't see why keeping this ROC around would be a potential exploit. Care to explain?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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.

Choose a reason for hiding this comment

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

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.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed that, if implemented, would solve this existing vulnerability.

@stiiifff stiiifff marked this pull request as ready for review September 6, 2023 11:15
WithdrawAsset(fee_asset),
BuyExecution { fees, weight_limit },
ReceiveTeleportedAsset(foreing_assets.clone()),
// Intentionally trap ROC to avoid exploit of draining Sovereing Account
Copy link
Contributor

@joepetrowski joepetrowski Sep 6, 2023

Choose a reason for hiding this comment

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

Doesn't look trapped?
IMO this is hacky. Just debit the user of ROC here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I would need some more explanation here as I do not fully understand where are you suggesting to debit the ROC from. Thanks!

Copy link
Contributor

Choose a reason for hiding this comment

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

I mean here on Trappist. You have reserve-backed ROC here, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No initially, but yes after #275 , so shall we move forward and merge 275 which solves this ?

// aware of this and implement a mechanism to prevent draining.
WithdrawAsset(fee_asset),
BuyExecution { fees, weight_limit },
ReceiveTeleportedAsset(foreing_assets.clone()),
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
ReceiveTeleportedAsset(foreing_assets.clone()),
ReceiveTeleportedAsset(foreing_assets.clone()),
RefundSurplus,

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

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.

Copy link
Contributor

Choose a reason for hiding this comment

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

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?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Indeed, so I think that #275 is establishing as the way to go.

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 },
Copy link
Contributor

Choose a reason for hiding this comment

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

Why not just AllOf whatever is left?

Copy link
Contributor

Choose a reason for hiding this comment

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

Also, this is depositing the Trappist token, but the comment says (and I believe is correct) that it should deposit ROC.

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 what you want.

Suggested change
DepositAsset { assets: MultiAssetFilter::Definite(foreing_assets), beneficiary },
DepositAsset { assets: AllCounted(2).into(), beneficiary }

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Agreed and initially did it that way but changed to depositing only teleported foreign_assets because of this.

pallets/withdraw-teleport/Cargo.toml Outdated Show resolved Hide resolved
@stiiifff
Copy link
Contributor

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).

@stiiifff stiiifff merged commit 479ffb2 into main Sep 15, 2023
@stiiifff stiiifff deleted the emi-proxy-teleport-pallet branch September 15, 2023 15:39
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants