3ddf58cef9
- Fix pezpallet-welati EnsureOrigin implementations (3 fixes)
- Remove incorrect #[cfg(not(feature = "runtime-benchmarks"))] blocks
- Affects EnsureSerok, EnsureParlementer, EnsureDiwan
- Fix asset-hub-zagros governance origins macros (2 fixes)
- Remove non-benchmark try_successful_origin from decl_unit_ensures!
- Remove non-benchmark try_successful_origin from decl_ensure!
- Rename snowbridge -> pezsnowbridge for consistency
- Update WORKFLOW_PLAN.md with build status and package names
- Correct package names: pezkuwi-teyrchain-bin, pezstaging-node-cli
- Mark completed builds: pezkuwi, pezkuwi-teyrchain-bin,
pezstaging-node-cli, teyrchain-template-node
357 lines
13 KiB
Markdown
357 lines
13 KiB
Markdown
# Snowbridge V2
|
|
|
|
This design lowers fees, improves UX, improves relayer decentralization and allows "transacting" over the bridge, making
|
|
it a general-purpose bridge rather than just a token bridge.
|
|
|
|
We're grateful to Adrian Catangiu, Francisco Aguirre, and others from the Parity XCM/Bridges team for their help and
|
|
collaboration on this design.
|
|
|
|
## Summary
|
|
|
|
- Unordered messaging
|
|
- All messages routed through AH
|
|
- Off-chain fee estimation
|
|
- P→E Fee Asset: WETH
|
|
- E→P Fee Asset: ETH
|
|
- Relayer rewards for both directions paid out on AH in WETH
|
|
|
|
## Pezkuwi→Ethereum
|
|
|
|
Given source teyrchain $S$, with native token $S^{'}$ and the initial xcm $x_0$ to be executed on $S$.
|
|
|
|
### Step 1: User agent constructs initial XCM
|
|
|
|
The user agent constructs an initial XCM message $x_0$ that will be executed on S.
|
|
|
|
The fee amounts in this message should be high enough to enable dry-running, after which they will be lowered.
|
|
|
|
### Step 2: User agent estimates fees
|
|
|
|
- Given source teyrchain $S$, with native token $S^{'}$ and the initial xcm $x_0$ to be executed on $S$.
|
|
- The native currency $P^{'}$ (HEZ) of the Pezkuwi relay chain, and $E^{'}$ (ETH) of Ethereum.
|
|
- Suppose that the user agent chooses relayer reward $r$ in $E^{'}$.
|
|
- Suppose that the exchange rates are $K_{P^{'}/S^{'}}$ and $K_{E^{'}/S^{'}}$. The user agent chooses a multiplier to
|
|
$\beta$ to cover volatility in these rates.
|
|
|
|
Apply the following sequence operations:
|
|
|
|
1. Dry-run $x_0$ on $S$ to receive xcm $x_1$ and cost $a$ in $S^{'}$
|
|
2. Dry-run $x_1$ on AH to receive xcm $x_2$ and cost $b$ in $P^{'}$ (HEZ)
|
|
3. Dry-run $x_2$ on BH to receive command $m$ and cost $c$ in $P^{'}$ (HEZ)
|
|
4. Dry-run $m$ on Ethereum to receive cost $d$ in $E^{'}$ (ETH)
|
|
|
|
The final cost to the user in $S^{'}$ is given by
|
|
|
|
$$
|
|
\beta \left(a + \frac{b + c}{K_{P^{'}/S^{'}}} + \frac{d + r}{K_{E^{'}/S^{'}}}\right)
|
|
$$
|
|
|
|
The user agent should perform a final update to xcm $x_0$, substituting the calculated fee amounts.
|
|
|
|
### Step 3: User agent initiates bridging operation
|
|
|
|
The user agent calls `pallet_xcm::execute` with the initial xcm $x_0$
|
|
|
|
```text
|
|
WithdrawAsset (KLT, 100)
|
|
PayFees (KLT, 20)
|
|
InitiateAssetsTransfer asset=(KLT, 60) remoteFee=(KLT, 20) dest=AH
|
|
ExchangeAsset give=(KLT, 20) want=(WETH, 1)
|
|
InitiateAssetsTransfer asset=(KLT, 40) remoteFee=(WETH, 1) dest=Ethereum
|
|
DepositAsset (KLT, 40) beneficiary=Bob
|
|
```
|
|
|
|
### Step 4: AH executes message x1
|
|
|
|
The message $x_1$ is application-specific:
|
|
|
|
```text
|
|
ReserveAssetDeposited (KLT, 80)
|
|
PayFees (KLT, 20)
|
|
SetAssetClaimer Kilt/Alice
|
|
AliasOrigin Kilt/Alice
|
|
ExchangeAsset give=(KLT, 20) want=(WETH, 1)
|
|
InitiateAssetsTransfer asset=(KLT, 60) remoteFee=(WETH, 1) dest=Ethereum
|
|
DepositAsset (KLT, 60) beneficiary=Bob
|
|
```
|
|
|
|
or
|
|
|
|
```text
|
|
*ReserveAssetDeposited (KLT, 80)
|
|
*PayFees (KLT, 20)
|
|
*SetAssetClaimer Kilt/Alice
|
|
*AliasOrigin Kilt/Alice
|
|
ExchangeAsset give=(KLT, 20) want=(WETH, 1)
|
|
InitiateAssetsTransfer asset=(KLT, 60) remoteFee=(WETH, 1) dest=Ethereum
|
|
DepositAsset (KLT, 60) beneficiary=Bob
|
|
Transact Bob.hello()
|
|
```
|
|
|
|
Note that the `SetAssetClaimer` instruction is placed before `AliasOrigin` in case AH fails to interpret the latter
|
|
instruction.
|
|
|
|
In all cases, $x_1$ should contain the necessary instructions to:
|
|
|
|
1. Pay fees for local execution using `PaysFees`
|
|
2. Obtain WETH for remote delivery fees.
|
|
|
|
The XCM bridge-router on AH will charge a small fee to prevent spamming BH with bridge messages. This is necessary since
|
|
the `ExportMessage` instruction in message $x_2$ will have no execution fee on BH. For a similar reason, we should also
|
|
impose a minimum relayer reward of at least the existential deposit 0.1 HEZ, which acts as a deposit to stop spamming
|
|
messages with 0 rewards.
|
|
|
|
### Step 5: BH executes message x2
|
|
|
|
Message $x_2$ is parsed by the `SnowbridgeMessageExporter` in block $n$ with the following effects:
|
|
|
|
- A bridge command $m$ is committed to binary merkle tree $M_n$.
|
|
- The transferred asset is parsed from `ReserveAssetDeposited` , `WithdrawAsset` or `TeleportedAssetReceived`
|
|
instructions for the local, destination and teleport asset transfer types respectively.
|
|
- The original origin is preserved through the `AliasOrigin` instruction. This will allow us to resolve agents for the
|
|
case of `Transact`.
|
|
- The message exporter must be able to support multiple assets and reserve types in the same message and potentially
|
|
multiple `Transacts`.
|
|
- The Message Exporter must be able to support multiple Deposited Assets.
|
|
- The Message Exporter must be able to parse `SetAssetClaimer` and allow the provided location to claim the assets on
|
|
BH in case of errors.
|
|
- Given relayer reward $r$ in WETH, set storage $P(\mathrm{hash}(m)) = r$. This is parsed from the `WithdrawAsset` and
|
|
`PayFees` instruction within `ExportMessage`.
|
|
|
|
Note that WETH on AH & BH is a wrapped derivative of the
|
|
[WETH](https://etherscan.io/token/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2) ERC20 contract on Ethereum, which is
|
|
itself a wrapper over ETH, the native currency of Ethereum. For the purposes of this document you can consider them all
|
|
to be of equivalent value.
|
|
|
|
```text
|
|
!WithdrawAsset(HEZ, 10)
|
|
!PayFees (HEZ, 10)
|
|
!ExportMessage dest=Ethereum
|
|
*ReserveAssetDeposited (KLT, 60)
|
|
*WithdrawAsset (WETH, 1)
|
|
*PayFees (WETH, 1)
|
|
*SetAssetClaimer Kilt/Alice
|
|
*AliasOrigin Kilt/Alice
|
|
DepositAsset (KLT, 60) beneficiary=Bob
|
|
```
|
|
|
|
or
|
|
|
|
```text
|
|
!WithdrawAsset(HEZ, 10)
|
|
!PayFees (HEZ, 10)
|
|
!ExportMessage dest=Ethereum
|
|
*ReserveAssetDeposited (KLT, 80)
|
|
*PayFees (KLT, 20)
|
|
*SetAssetClaimer Kilt/Alice
|
|
*AliasOrigin Kilt/Alice
|
|
DepositAsset (KLT, 60) beneficiary=Bob
|
|
Transact Bob.hello()
|
|
```
|
|
|
|
### Step 6: Relayer relays message to Gateway
|
|
|
|
1. A relayer _Charlie_ inspects storage $P$ to look for new messages to relay. Suppose it finds $\mathrm{hash}(m)$
|
|
giving reward $r$.
|
|
2. The relayer queries $m$ from $M$ and constructs the necessary proofs.
|
|
3. The relayer dry-runs m on Ethereum to decide whether the message is profitable to deliver.
|
|
4. The relayer finally delivers the message together with a relayer-controlled address $u$ on AH where the relayer can
|
|
claim their reward after proof of delivery.
|
|
|
|
### Step 7: Relayer delivers proof of delivery to BH
|
|
|
|
The proof of delivery is essentially a merkle proof for the `InboundMessageAccepted` event log.
|
|
|
|
When BH processes the proof of delivery:
|
|
|
|
1. The command $m$ is removed from storage items $M$ and $P$.
|
|
2. The relayer reward is tracked in storage $R$, where $R(u)$ is the accumulated rewards that can be claimed by account
|
|
$u$.
|
|
|
|
## Ethereum→Pezkuwi
|
|
|
|
### Step 1: Submit send on Gateway
|
|
|
|
The interface that the Gateway will use to initiate transfers will be similar to the interface from
|
|
`transfer_assets_using_type_and_then` extrinsic that we currently use to initiate transfers from the Pezkuwi to
|
|
Ethereum direction.
|
|
|
|
1. It must allow multiple assets to be transferred and specify the transfer type: Local, Destination or Teleport asset
|
|
transfer types. It is the job of the User Agent/UX layer to fill in this information correctly.
|
|
2. It must allow specifying a destination which is `Address32`, `Address20` or a custom scale-encoded XCM payload that
|
|
is executed on the destination. This is how we will support `Transact` , the User Agent/UX layer can build a
|
|
scale-encoded payload with an encoded transact call.
|
|
3. The same interface is used for both PNA (Pezkuwi Assets) and ERC20 tokens. Internally we will still look up whether
|
|
the token is registered as a PNA or ERC20 for the purpose of minting/locking burning/unlocking logic. The asset
|
|
transfer type chosen by the UX layer will inform the XCM that is built from the message on BH.
|
|
|
|
```solidity
|
|
enum Kind {
|
|
Index,
|
|
Address32,
|
|
Address20,
|
|
XCMPayload,
|
|
}
|
|
|
|
struct Beneficiary {
|
|
Kind kind;
|
|
bytes data;
|
|
}
|
|
|
|
enum AssetTransferType {
|
|
ReserveDeposit, ReserveWithdraw, Teleport
|
|
}
|
|
|
|
struct Token {
|
|
AssetTransferType type;
|
|
address token;
|
|
uint128 amount;
|
|
}
|
|
|
|
function send(
|
|
ParaID destinationChain,
|
|
Beneficiary calldata beneficiary,
|
|
Token[] tokens,
|
|
uint128 reward
|
|
) external payable;
|
|
```
|
|
|
|
Message enqueued $m_0$:
|
|
|
|
```solidity
|
|
send(
|
|
3022, // KILT Para Id
|
|
Address32(0x0000....),
|
|
[(ReserveWithdraw, KLT, 100)],
|
|
10, // WETH
|
|
)
|
|
```
|
|
|
|
```solidity
|
|
send { value: 3 }( // Send 3 Eth for fees and reward
|
|
3022, // KILT Para Id
|
|
XCMPayload(
|
|
DepositAsset (KLT, 100) dest=Bob
|
|
Transact Bob.hello()
|
|
),
|
|
[(ReserveWithdraw, KLT, 100)],
|
|
1, // 1 ETH of 3 needs to be for the reward, the rest is for fees
|
|
)
|
|
```
|
|
|
|
The User Agent/UX layer will need to estimate the fee required to be passed into the `send` method. This may be an issue
|
|
as we cannot Dry-Run something on Pezkuwi that has not even been submitted on Ethereum yet. We may need to make RPC API
|
|
to DryRun and get back the xcm that would be submitted to asset hub.
|
|
|
|
### Step 2: Relayer relays message to Bridge Hub
|
|
|
|
On-chain exchange rate is eliminated. Users pay remote delivery costs in ETH, and this amount is sent with the message
|
|
as WETH. The delivery fee can be claimed by the relayer on BH.
|
|
|
|
The user agent applies a similar dry-running process as with
|
|
[Step 2: User agent estimates fees](https://www.notion.so/Step-2-User-agent-estimates-fees-113296aaabef8159bcd0e6dd2e64c3d0?pvs=21).
|
|
|
|
The message is converted from $m_0$ to $x_0$ during message submission on BH. Dry-running submission will return $x_0$
|
|
to the relayer so that it can verify it is profitable.
|
|
|
|
### Step 3: AH receives $x_0$ from BH
|
|
|
|
Submitting the message $m_0$ will cause the following XCM, $x_0$, to be built on BH and dispatched to AH.
|
|
|
|
```text
|
|
WithdrawAsset (KLT, 100)
|
|
ReserveAssetDeposited(WETH, 2)
|
|
PayFees (WETH, 1)
|
|
SetAssetClaimer Kilt/Bob // derived from beneficiary on final destination
|
|
AliasOrigin Ethereum/Alice // derived from msg.sender
|
|
InitiateAssetsTransfer asset=(KLT, 100) remoteFee=(WETH, 1) dest=KLT
|
|
DepositAsset (KLT, 100) beneficiary=Bob
|
|
```
|
|
|
|
```text
|
|
WithdrawAsset (KLT, 100)
|
|
ReserveAssetDeposited(WETH, 2)
|
|
PayFees (WETH, 1)
|
|
SetAssetClaimer Kilt/Bob // derived from beneficiary on final destination
|
|
AliasOrigin Ethereum/Alice // derived from msg.sender
|
|
InitiateAssetsTransfer asset=(KLT, 100) remoteFee=(WETH, 1) dest=KLT
|
|
DepositAsset (KLT, 100) beneficiary=Bob
|
|
Transact Bob.hello()
|
|
```
|
|
|
|
### Step 4: KILT Receives XCM from AH
|
|
|
|
The following XCM $x_1$ is received from AH on KILT.
|
|
|
|
```text
|
|
*WithdrawAsset (KLT, 100)
|
|
*ReserveAssetDeposited (WETH, 1)
|
|
*PayFees (WETH, 1)
|
|
*SetAssetClaimer Ethereum/Alice
|
|
*AliasOrigin Ethereum/Alice // origin preserved from AH
|
|
SetAssetClaimer Bob
|
|
DepositAsset (KLT, 100) beneficiary=Bob
|
|
```
|
|
|
|
```text
|
|
*WithdrawAsset (KLT, 100)
|
|
*ReserveAssetDeposited (WETH, 1)
|
|
*PayFees (WETH, 1)
|
|
*SetAssetClaimer Ethereum/Alice
|
|
*AliasOrigin Ethereum/Alice // origin preserved from AH
|
|
SetAssetClaimer Bob
|
|
DepositAsset (KLT, 100) beneficiary=Bob
|
|
Transact Bob.hello() // executes with the origin from AH
|
|
```
|
|
|
|
## Relayer Rewards
|
|
|
|
The tracking and disbursement of relayer rewards for both directions has been unified. Rewards are accumulated on BH in
|
|
WETH and must be manually claimed. As part of the claims flow, an XCM instruction is sent to AH to mint the WETH into
|
|
the deposit account chosen by the relayer.
|
|
|
|
To claim, call following extrinsic, where $o$ is rewards account (origin), and $w$ is account on AH where the WETH will
|
|
be minted.
|
|
|
|
$$
|
|
\mathrm{claim}(o,w)
|
|
$$
|
|
|
|
For tax accounting purposes it might be desirable that $o \neq w$.
|
|
|
|
## Top-Up
|
|
|
|
Top-up of the relayer reward is viable to implement for either direction as extrinsics on Bridge Hub and Ethereum
|
|
respectively.
|
|
|
|
## Origin Preservation
|
|
|
|
Origins for transact will be preserved by use of the `AliasOrigin` instruction. This instruction will have the following
|
|
rules that teyrchain runtimes will need to allow:
|
|
|
|
1. `AliasOrigin` can behave like `DescendOrigin`. This is safe because it respects the hierarchy of multi-locations and
|
|
does not allow jumping up. Meaning no escalation of privileges.
|
|
1. Example location `Ethereum` can alias into `Ethereum/Alice` because we are descending in origin and this
|
|
essentially is how the `DescendOrigin` instruction works.
|
|
2. `AliasOrigin` must allow AH to alias into bridged locations such as
|
|
`{ parents: 2, interior: GlobalConsensus(Ethereum) }` and all of its internal locations so that AH can act as a proxy
|
|
for the bridge on teyrchains.
|
|
|
|
`AliasOrigin` will be inserted by every `InitiateAssetTransfer` instruction on the source teyrchain, populated with the
|
|
contents of the origin register, essentially forwarding the origin of the source to the destination.
|
|
|
|
RFCS:
|
|
|
|
[https://github.com/polkadot-fellows/RFCs/pull/122](https://github.com/polkadot-fellows/RFCs/pull/122)
|
|
|
|
[https://github.com/polkadot-fellows/RFCs/blob/main/text/0100-xcm-multi-type-asset-transfer.md](https://github.com/polkadot-fellows/RFCs/blob/main/text/0100-xcm-multi-type-asset-transfer.md)
|
|
|
|
## Teyrchain Requirements
|
|
|
|
1. Pezpallet-xcm.execute enabled.
|
|
2. XCM payment and dry run apis implemented.
|
|
3. Must accept WETH needed for fees. Though in future user agents can inject `ExchangeAsset` instructions to obtain
|
|
WETH.
|
|
4. Trust AH as a reserve for bridged assets.
|
|
5. Origin Preservation rules configured which allow asset hub to impersonate bridged addresses.
|