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

Snowbridge v2 - Inbound Queue #6697

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

Conversation

claravanstaden
Copy link
Contributor

@claravanstaden claravanstaden commented Nov 28, 2024

Description

Implements the Inbound Queue for Snowbridge v2.

Integration

This PR adds:

  • A new pallet called EthereumInboundQueueV2 with a submit extrinsic.
  • A new runtime API called InboundQueueApiV2 with a method dry_run, that takes an Ethereum command and converts it to Xcm and provides the execution fee on BH and static fee part of the AH execution.

Since this is a new pallet, no breaking changes are made to the EthereumInboundQueue pallet.

Review Notes

The Inbound Queue v2 pallet expects an abi-encoded envelope from Ethereum. Once decoded, the payload parameter is SCALE decoded into a v2::Message. The message contains:

  • origin: The origin address of the user on Ethereum
  • assets: A vector of assets that will be placed in the holding on AH. Can be a native ERC-20 or a foreign ERC-20 (Polkadot native assets). The XCM instructions that are used differ per asset type (ReserveAssetDeposited for native ERC-20 tokens and WithdrawAsset for Polkadot native assets). The user on Ethereum specifies additional xcm deposit the asset into a beneficiary account.
  • xcm: User provided xcms, to deposit the asset to a beneficiary, provide further fees or other instructions such as Transact (for example, to register a token)
  • claimer: The claimer on AH, in case funds are trapped so it can be claimed.

The relayer pays a DOT fee that covers:

  • The execution fees on BH
  • The static xcm part of the message on AH

The DOT fee is burnt from the relayer account and teleported to AH.

Messages may be processed out of order. Nonces are stored in a sparse bitmap for space-efficient storage.

@claravanstaden claravanstaden changed the title Snowbridge Unordered Message Delivery - Inbound Queue Snowbridge v2 - Inbound Queue Nov 29, 2024

let mut origin_location = Location::new(2, [GlobalConsensus(network)]);
if message.origin == GatewayProxyAddress::get() {
// If the message origin is the gateway proxy contract, set the origin to
Copy link
Contributor

Choose a reason for hiding this comment

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

You do not need this branch. The origin is already set here:

			UniversalOrigin(GlobalConsensus(network)),

https://github.com/paritytech/polkadot-sdk/pull/6697/files#diff-011a2c280539a541db601aa2fd4e5f30960525c4bb35588c3de78effb01720fcR147

So you can only add AliasOrigin if its not the gateway proxy.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Changed in 02750d6.

type Balance = Balance;
type MessageConverter = snowbridge_router_primitives::inbound::v2::MessageToXcm<
EthereumNetwork,
ConstU8<INBOUND_QUEUE_PALLET_INDEX>,
Copy link
Contributor

Choose a reason for hiding this comment

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

This pallet index is for the inbound queue v1 pallet. We are essentially re-using it for the v2 pallet and I am not sure if this is the right thing to do. We may need to add the v2 pallet index separately.

This is used to allow the use of the UniversalOrigin instruction, which only the v1 pallet is configured to use now.

Its set in xcm config here:

type UniversalAliases =
(bridging::to_rococo::UniversalAliases, bridging::to_ethereum::UniversalAliases);

And bridge config here:

(SiblingBridgeHubWithEthereumInboundQueueInstance::get(), GlobalConsensus(EthereumNetwork::get().into())),

Basically it means only the bridge hub inbound queue v1 pallet can be the Universal origin { parents: 2, interior: X1([GlobalConsensus(Ethereum { chain_id: 11155111 }) } (Snowbridge Sovereign)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Addressed in 40e85ea.

@@ -12,6 +12,7 @@ workspace = true

[dependencies]
hex-literal = { workspace = true, default-features = true }
hex = { workspace = true, default-features = true }
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 like a dev dependency only?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This whole crate is tests only and then all dependencies should be under dev-dependencies, I think.

Copy link
Contributor

Choose a reason for hiding this comment

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

AH ok, ignore then.

Self::deposit_event(Event::MessageReceived { nonce: envelope.nonce, message_id });

// Set nonce flag to true
Nonce::<T>::set(envelope.nonce.into());
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if re-entrancy is a potential security issue for substrate pallets like it is for smart contracts, but generally you want to increment (Or in this case set) the nonce first and then dispatch.

See this comment:
https://github.com/Snowfork/snowbridge/pull/1300/files#diff-74470ba8f35b42dbc8805739e92708c0696343e0b1819feb3e7a5ebb77cc623cR140-R143

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 a good point, d70ac6c.

&mut message.xcm.as_ref(),
)
.map_err(|_| ConvertMessageError::InvalidVersionedXCM)?;
message_xcm = versioned_xcm.try_into().map_err(|_| ConvertMessageError::InvalidXCM)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

A possible alternative to failing here could be let the message go through with a empty xcm payload, which will essentially trap the funds on assethub, allowing the user to claim them. Lets discuss this.

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 would be more work for the user though. I think it's better to fail early and not move any tokens around than allowing it and the user needs to take corrective action. Unless you see a benefit to doing this?

Copy link
Contributor

Choose a reason for hiding this comment

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

The funds are already locked on the Ethereum side so tokens are moved by this point. If the xcm is malformed for any reason there is no corrective step that can be applied here. The message is immutable and nothing can make the xcm well formed once submitted on the etherum side.

So the options for malformed xcm are:

  1. Return error and revert: Funds are trapped on ethereum, no way to push the message through. The message is undeliverable due to the revert.
  2. Push message to Asset Hub with empty xcm: The funds get trapped on asset hub where there is a claim process. If the user does want to do the extra step of claiming, then the funds are essentially still trapped, but on asset hub where it is claimable in some form.
  3. Maybe if the bug is in our code, we could apply a fix to the inbound-queue-v2 and go through the whole release process, and a message could be retried after the release is done.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see what you mean, thanks for explaining. I have changed it to allow invalid xcm: 6af760b

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I did the same for an invalid claimer payload: b368760 If the claimer bytes cannot be decoded into an Xcm junction, don't add the claimer to the Xcm. The relayer will then be set to the claimer, as you mentioned here: #6697 (comment)

What do you think about foreign ERC20 (FNB) tokens that cannot be converted into a asset ID? Should we error? Or not add to the assets vector?

AccountKey20 { network: None, key: (*token_id).into() },
],
);
instructions.push(ReserveAssetDeposited((token_location, *value).into()));
Copy link
Contributor

Choose a reason for hiding this comment

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

ReserveAssetDeposited takes multiple Assets as an argument. Instead of using multiple ReserveAssetDeposited with a single asset in each, do one ReserveAssetDeposited with multiple assets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

let asset_id = ConvertAssetId::convert(&token_id)
.ok_or(ConvertMessageError::InvalidAsset)?;
instructions.push(WithdrawAsset((asset_id, *value).into()));
},
Copy link
Contributor

Choose a reason for hiding this comment

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

Same here with WithdrawAssets. Pass in multiple assets.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

}
}

if let Some(claimer) = message.claimer {
Copy link
Contributor

Choose a reason for hiding this comment

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

Set the claimer before ReserveDepositing or Withdrawing assets so that in the case of a failure, the user can at least claim some of their funds.

Copy link
Contributor Author

@claravanstaden claravanstaden Dec 5, 2024

Choose a reason for hiding this comment

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

origin_location
.push_interior(AccountKey20 { key: message.origin.into(), network: None })
.map_err(|_| ConvertMessageError::InvalidOrigin)?;
instructions.push(AliasOrigin(origin_location.into()));
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 potentially use DescendOrigin here instead of AliasOrigin. This is because AliasOrigin in this case is essentially doing a descend origin i.e. appending the msg.sender to the existing origin.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed, it is better. 9ffb3e1

// Refund excess fees to the relayer
DepositAsset {
assets: Wild(AllOf { id: AssetId(fee_asset.into()), fun: WildFungible }),
beneficiary: origin_account_location,
Copy link
Contributor

Choose a reason for hiding this comment

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

We should maybe refund to the claimer if its present, and if its not than refund to the relayer. The relayer will get its reward separately.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants