# The Avail Bridge (Public Copy)
This document describes the specification for an arbitrary message bridge to and fro, Avail ↔ Ethereum.
## Background
The bridge is primarily designed as a means to bridge `AVAIL` tokens to-and-fro Ethereum, this can be expanded into other tokens, with other runtime upgrades, however, to allow for a generalized interface that can be expanded into different use-cases, an arbitrary message bridge on Ethereum is ideal.
## Links
- [https://github.com/availproject/contracts](https://github.com/availproject/contracts)
- [https://github.com/succinctlabs/vectorx](https://github.com/succinctlabs/vectorx)
- [https://docs.telepathy.xyz/](https://docs.telepathy.xyz/)
- [https://github.com/succinctlabs/telepathy-contracts/tree/main/src/amb](https://github.com/succinctlabs/telepathy-contracts/tree/main/src/amb)
- [https://github.com/succinctlabs/telepathy-contracts/tree/main/external/examples/bridge](https://github.com/succinctlabs/telepathy-contracts/tree/main/external/examples/bridge)
- [Succinct ZK Bridge](https://www.notion.so/Succinct-ZK-Bridge-602c166b657b45b99043ac2fdb92194e?pvs=21)
## Proposal
We propose the following features:
- A pallet called `Bridge` that hooks into building the `dataRoot` for a block for transferring data and tokens from Avail to Ethereum.
- `AVAIL` transfers emit corresponding messages on Ethereum which allows unlocks to an address on Avail by decoding the data transmitted in the message.
- Consequently, the formation of `dataRoot` is now altered (see below).
- A wrapper around the `VectorX` contracts to allow bridging data from Avail using `dataRoot`.
- A helper function that allows for checking if data attestation proofs are valid
- A helper function that allows for checking if bridging proofs are valid
- A external function that transmits arbitrary data to a designated recipient from Avail to Ethereum
### Data root upgrade specification
We propose that the `dataRoot` for a block is modified such that:
$dataRoot = \texttt{keccak256}(\begin{cases}
\texttt{0x00000000000000000000000000000000} & \quad \text{when $\emptyset$}\\
blobRoot & \quad \text{otherwise}
\end{cases}\texttt{,}\\\begin{cases}
\texttt{0x00000000000000000000000000000000} & \quad \text{when $\emptyset$}\\
bridgeRoot & \quad \text{otherwise}
\end{cases})$
Where the $blobRoot$ is effectively the $dataRoot$ in the current Avail implementation. It is imperative that this ordering is maintained consistently and empty trees are denoted as such in the composed data root hash.
### Domain specification
We propose a concept called $domain$ similar to domain separators in [EIP-712](https://eips.ethereum.org/EIPS/eip-712). The $domain$ in a message serves as delimiter to distinguish messages for different domains. This is required in case we plan to support other chains, or deprecate chains, and distinguish between different networks and/or different kind of messages. For the bridge, we propose the following domains:
| Chain | Domain |
| --- | --- |
| Avail | 1 |
| Ethereum | 2 |
| Polkadot (hypothetical!) | 3 |
### Message type specification
We define customized message types to allow for customized behaviour in bridge contract(s) and in the pallet runtime during extrinsic execution. We can define an `enum` along the same lines for the pallet that maps the extrinsic behaviour.
```rust
pub enum Message {
ArbitraryMessage(BoundedData), // must not exceed 100KB (Ethereum calldata limits)
FungibleToken {
asset_id: H256,
amount: u128,
},
}
```
| Message type | Message prefix | Enum |
| --- | --- | --- |
| Arbitrary message | 0x01 | ArbitraryMessage |
| ERC-20-like token transfer | 0x02 | FungibleToken |
| ERC-721-like token transfer (hypothetical!) | 0x03 | NonFungibleToken |
A single-byte prefix is prepended to the ABI-encoded leaves based on the specification above.
### Asset ID specification
Different tokens on different domains can be denoted as an $Asset$ with a special `AssetID` to facilitate ERC-20-like token transfers across the bridge. The `AVAIL` token is the default network token that originates on the Avail mainnet network, hence we enshrine an asset ID as the zero hash.
| Asset | Asset ID |
| --- | --- |
| AVAIL | 0x0000000000000000000000000000000000000000000000000000000000000000 |
| ETH | 0x4554480000000000000000000000000000000000000000000000000000000000 |
We derive the `ETH` asset ID by simply representing the UTF-8 string “ETH” as a hexadecimal. Since it is unlikely for token tickers to collide, this is a simple enough approach. For token upgrades, we can derive the asset ID with a scheme like the `bytes32(abi.encodePacked("$TOKEN_VERSION"))` and deprecate the older asset ID.
### Bridging extrinsics specification
We propose adding extrinsic that is supported by the `Bridge` pallet along the following lines:
```rust
send_message(
origin: OriginFor<T>,
message: Message,
to: H256,
#[pallet::compact]
domain: u32,
)
```
The pallet expects either of `asset_id` **and** `amount` **or** just `data`, depending on which the appropriate leaf type gets encoded into the message.
The pallet has to check for the following:
- Is domain valid?
- i.e. $originDomain \neq destinationDomain$
- Is the domain supported?
- Currently, only `2` for Ethereum
- Is the asset ID supported?
- Currently, only `0x0000000000000000000000000000000000000000000000000000000000000000` for `AVAIL` and `0x4554480000000000000000000000000000000000000000000000000000000000` for ETH will be implemented later on the pallet.
- Is the `amount` transferable?
Assuming a token price of $10, we *can* have a floor of 0.01 `AVAIL` as the $extrinsicFee$ and scale it based on the size of the message.
Any $amount > 0$ is transferred to the designated pallet address, which can be derived from:
```rust
impl TypeId for PalletId {
const TYPE_ID: [u8; 4] = *b"modl";
}
pub const BridgePalletId: PalletId = PalletId(*b"avl/brdg");
```
This gives us an account ID: `0x6d6f646c61766c2f627264670000000000000000000000000000000000000000` which maps to the Substrate address, `5EYCAe5fjGT2va2dXMcAmfdFMjnESUd4Pu6Cqb4VttbuSzkX`.
If `asset_id = 0x0000000000000000000000000000000000000000000000000000000000000000`:
- `amount` of `AVAIL` tokens are locked in the pallet address.
Else:
- `amount` of $Asset$ is burned from the pallet address.
Internally, the `Bridge` pallet transforms the extrinsic into a message like:
```rust
pub struct AddressedMessage {
pub message: Message,
pub from: H256, // AccountId of the account that submitted the extrinsic
pub to: H256,
#[codec(compact)]
pub origin_domain: u32,
#[codec(compact)]
pub destination_domain: u32,
#[codec(compact)]
pub id: u64, /// Unique identifier for the message
}
```
If `messageType = 0x01` also check:
- Does the data exceed allowed limits?
- If `value` or `asset_id` fields are specified, extrinsic submission *must* fail.
If `messageType = 0x02` then check:
- `data` field must be ABI-encoded before getting put in the leaf: `abi.encode(asset_id, value)`
- If `assetId = 0x0`:
- Is the amount field consistent with the value transferred, i.e. does the account have a balance that exceeds $tip + extrinsicFee + amount$
- Else:
- Is the value field consistent with the value transferred, i.e. does the account have a balance of $Asset$ that exceeds $amount$
- If the pallet address does not have more than or equal to the $amount$ of $Asset$, extrinsic submission *must* fail.
- If `data` field is specified, extrinsic submission *must* fail.
### Blob root construction specification
We alter $blobRoot$ construction along the following lines:
- Hash the blob to create the $preliminaryLeafHash$, this should be a valid `H256` keccak hash.
- Rehash the preliminary leaf like $keccak256(preliminaryLeafHash)$
- Include the leaf as part of the $blobRoot$
On the Avail bridge, we require $preliminaryLeafHash$ to be revealed, the contract hashes it again to check the generated leaf for inclusion in $blobRoot$. Any valid proof of the generated leaf would inherently prove that the pre-image exists (because you need to reveal it), consequently proving that the pre-image of that also exists, i.e. the blob was included. From a cryptographical standpoint, it is sufficiently secure because assuming the adverse scenario that an internal node is passed as a leaf, it is virtually impossible, that once rehashed, it is possible to construct a valid proof of inclusion in $blobRoot$.
### Bridging root construction specification
The $message$ is then ABI-encoded (in an **unpacked** fashion) and hashed using $keccak256$ similar to other data root leaves. We then construct a Merkle tree of all the bridging leaves in a block. It is important that the encoding is done according to the ABI specification *only* to prevent type coalescing and spilling over arbitrary types into other fields.
The pallet ***must*** validate ******that the leaves are ordered, unique and monotonically increasing by 1 *only* (checking the last condition is enough)*,* failing which the block must be treated as invalid. Therefore, check for the following:
- Get the latest leaf ID from the previous block $N$
- The next leaf ID in the current block must be $I=N+1$, set $N$ to $N + 1$, and iterate till the last leaf.
- If at any point leaf id $I ≠ N + 1$, treat the block as invalid.
In this case, leaf IDs are ordered by the transaction index in the block.
We need a different tree for $bridgeRoot$ because all leaves that are part of the $bridgeRoot$ are authenticated, as they are constructed by the pallet. It is possible for any leaf that is part of the $blobRoot$ to coalesce or represent itself as a $bridgeRoot$ leaf (and vice versa) if the trees are not built separately, to prevent this exploit scenario, we need two separate Merkle trees to construct the $dataRoot$.
%20a00c2aa4937d496ea346d02a6bb119ff/Screenshot_2024-01-16_at_19.50.53.png)
%20a00c2aa4937d496ea346d02a6bb119ff/Untitled.png)
Data root is built from 2 sub-tries, *blob* sub-tree containing `submitted` data transactions and *bridge* sub-tree containing `bridge` transactions.
For example, when submitting 3 data transactions in the block, the data root should be calculated as follows:
Data: "0", "1", "2"
- Calculate `keccak256` from submitted data transactions
- Add leaf(s) to the left subtree and calculate `keccak256` again from the `submitted` data and construct blob root.
- If a number of leaf element(s) is not `2^n` append *x* number of elements `0x0..0` (in case of blobs not double hashed) so that trie is balanced.
- Build blob root from the existing leaf values.
- If the bridge sub-tree does not contain any leaf values use `0x0..0` as a bridge root.
- If a bridge sub-trie contains elements, construct a bridge root using message hash values.
Example of calculating a blob trie root where bridge root elements do not exist and the trie is balanced:
```rust
// leaf 0 keccak256(044852b2a670ade5407e78fb2863c51de9fcb96542a07186fe3aeda6bb8a116d)
// repeated hash of leaf 0 40105d5bc10105c17fd72b93a8f73369e2ee6eee4d4714b7bf7bf3c2f156e601
// leaf 1 keccak256(c89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc6)
// repeated hash of leaf 1 4aeff0db81e3146828378be230d377356e57b6d599286b4b517dbf8941b3e1b2
// leaf 2 keccak256(ad7c5bef027816a800da1736444fb58a807ef4c9603b7848673f7e3a68eb14a5)
// repeated hash of leaf 2 1204b3dcd975ba0a68eafbf4d2ca0d13cc7b5e3709749c1dc36e6e74934270b3
// leaf appended in order to have 2^n number of leaves
// leaf 3 (appended) keccak256(0000000000000000000000000000000000000000000000000000000000000000)
// 290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
// intermediate root (leaf[0], leaf[1]) db0ccc7a2d6559682303cc9322d4b79a7ad619f0c87d5f94723a33015550a64e
// intermediate root (leaf[2], leaf[3]) 3c86bde3a90d18efbcf23e27e9b6714012aa055263fe903a72333aa9caa37f1b
// data_root keccak256(db0ccc7a2d6559682303cc9322d4b79a7ad619f0c87d5f94723a33015550a64e, 3c86bde3a90d18efbcf23e27e9b6714012aa055263fe903a72333aa9caa37f1b)
// (877f9ed6aa67f160e9b9b7794bb851998d15b65d11bab3efc6ff444339a3d750)// if bridge root is none.
// data_root keccak256(blob_root, 0x0..0)
// if blob root is none.
// data_root keccak256(0x0..0, bridge_root)
// if blob root and bridge root are not none
// data_root keccak256(blob_root, bridge_root)
// result data root is:
// keccak256(877f9ed6aa67f160e9b9b7794bb851998d15b65d11bab3efc6ff444339a3d750,0000000000000000000000000000000000000000000000000000000000000000)
// data root: a3febf835ab07e66f2fb1bd0b962950bbfaa21b7f9d67635d587f9eeab0b7fe5
```
When the data root is calculated it will be part of the header in the block in `extension -> v3 -> commitment -> data_root`.
Querying data root can be done by calling Kate RPC `queryDataProof(transaction_index, at)` which will return the response:
```rust
pub struct ProofResponse {
pub data_proof: DataProof,
pub message: Option<AddressedMessage>,
}
pub struct DataProof {
pub roots: TxDataRoots,
/// Proof items (does not contain the leaf hash, nor the root obviously).
///
/// This vec contains all inner node hashes necessary to reconstruct the root hash given the
/// leaf hash.
pub proof: Vec<H256>,
/// Number of leaves in the original tree.
///
/// This is needed to detect a case where we have an odd number of leaves that "get promoted"
/// to upper layers.
#[codec(compact)]
pub number_of_leaves: u32,
/// Index of the leaf the proof is for (0-based).
#[codec(compact)]
pub leaf_index: u32,
/// Leaf content.
pub leaf: H256,
}
pub struct TxDataRoots {
/// Global Merkle root
pub data_root: H256,
/// Merkle root hash of submitted data
pub blob_root: H256,
/// Merkle root of bridged data
pub bridge_root: H256,
}
pub struct AddressedMessage {
pub message: Message,
pub from: H256,
pub to: H256,
#[codec(compact)]
pub origin_domain: u32,
#[codec(compact)]
pub destination_domain: u32,
/// Unique identifier for the message
#[codec(compact)]
pub id: u64,
}
pub enum Message {
ArbitraryMessage(BoundedData),
FungibleToken {
asset_id: H256,
#[codec(compact)]
amount: u128,
},
}
```
The response can be used to reconstruct the root and validate that blob data exist or the bridge data exists on the Avail. RPC method `kate_queryDataProof` that returns the response can be found [here](https://github.com/availproject/avail/blob/main/rpc/kate-rpc/src/lib.rs#L58).
### Ethereum bridge specification
The Ethereum bridge will have functions that are able to read the $dataRoot$ from headers submitted on the `VectorX` contract, and provide helper functions to verify if Merkle proofs submitted are valid, and if a bridging proof is valid, allow the trusted Ethereum bridge to message call a designated destination address on Ethereum with some data. The transaction itself can be sent by anyone but the message will be relayed only once (successfully).
```solidity
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.23;
import {Initializable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/Initializable.sol";
import {ReentrancyGuardUpgradeable} from
"lib/openzeppelin-contracts-upgradeable/contracts/utils/ReentrancyGuardUpgradeable.sol";
import {PausableUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/utils/PausableUpgradeable.sol";
import {AccessControlDefaultAdminRulesUpgradeable} from
"lib/openzeppelin-contracts-upgradeable/contracts/access/extensions/AccessControlDefaultAdminRulesUpgradeable.sol";
import {SafeERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
import {Merkle} from "src/lib/Merkle.sol";
import {IVectorx} from "src/interfaces/IVectorx.sol";
import {IWrappedAvail} from "src/interfaces/IWrappedAvail.sol";
import {IMessageReceiver} from "src/interfaces/IMessageReceiver.sol";
import {IAvailBridge} from "src/interfaces/IAvailBridge.sol";
/**
* @author @QEDK (Avail)
* @title AvailBridge
* @notice An arbitrary message bridge between Avail <-> Ethereum
* @custom:security security@availproject.org
*/
contract AvailBridge is
Initializable,
ReentrancyGuardUpgradeable,
PausableUpgradeable,
AccessControlDefaultAdminRulesUpgradeable,
IAvailBridge
{
using Merkle for bytes32[];
using SafeERC20 for IERC20;
bytes1 private constant MESSAGE_TX_PREFIX = 0x01;
bytes1 private constant TOKEN_TX_PREFIX = 0x02;
uint32 private constant AVAIL_DOMAIN = 1;
uint32 private constant ETH_DOMAIN = 2;
uint256 private constant MAX_DATA_LENGTH = 102_400;
// Derived from abi.encodePacked("ETH")
// slither-disable-next-line too-many-digits
bytes32 private constant ETH_ASSET_ID = 0x4554480000000000000000000000000000000000000000000000000000000000;
bytes32 private constant PAUSER_ROLE = keccak256("PAUSER_ROLE");
// map store spent message hashes, used for Avail -> Ethereum messages
mapping(bytes32 => bool) public isBridged;
// map message hashes to their message ID, used for Ethereum -> Avail messages
mapping(uint256 => bytes32) public isSent;
// map Avail asset IDs to an Ethereum address
mapping(bytes32 => address) public tokens;
IVectorx public vectorx;
IWrappedAvail public avail;
address public feeRecipient;
uint256 public fees; // total fees accumulated by bridge
uint256 public feePerByte; // in wei
uint256 public messageId; // next nonce
modifier onlySupportedDomain(uint32 originDomain, uint32 destinationDomain) {
if (originDomain != AVAIL_DOMAIN || destinationDomain != ETH_DOMAIN) {
revert InvalidDomain();
}
_;
}
modifier onlyTokenTransfer(bytes1 messageType) {
if (messageType != TOKEN_TX_PREFIX) {
revert InvalidFungibleTokenTransfer();
}
_;
}
modifier checkDestAmt(bytes32 dest, uint256 amount) {
if (dest == 0x0 || amount == 0) {
revert InvalidDestinationOrAmount();
}
_;
}
/**
* @notice Initializes the AvailBridge contract
* @param newFeePerByte New fee per byte value
* @param newFeeRecipient New fee recipient address
* @param newAvail Address of the WAVAIL token contract
* @param governance Address of the governance multisig
* @param pauser Address of the pauser multisig
* @param newVectorx Address of the VectorX contract
*/
function initialize(
uint256 newFeePerByte,
address newFeeRecipient,
IWrappedAvail newAvail,
address governance,
address pauser,
IVectorx newVectorx
) external initializer {
feePerByte = newFeePerByte;
// slither-disable-next-line missing-zero-check
feeRecipient = newFeeRecipient;
vectorx = newVectorx;
avail = newAvail;
__AccessControlDefaultAdminRules_init(0, governance);
_grantRole(PAUSER_ROLE, pauser);
__Pausable_init();
__ReentrancyGuard_init();
}
/**
* @notice Updates pause status of the bridge
* @param status New pause status
*/
function setPaused(bool status) external onlyRole(PAUSER_ROLE) {
if (status) {
_pause();
} else {
_unpause();
}
}
/**
* @notice Update the address of the VectorX contract
* @param newVectorx Address of new VectorX contract
*/
function updateVectorx(IVectorx newVectorx) external onlyRole(DEFAULT_ADMIN_ROLE) {
vectorx = newVectorx;
}
/**
* @notice Function to update asset ID -> token address mapping
* @dev Only callable by governance
* @param assetIds Asset IDs to update
* @param tokenAddresses Token addresses to update
*/
function updateTokens(bytes32[] calldata assetIds, address[] calldata tokenAddresses)
external
onlyRole(DEFAULT_ADMIN_ROLE)
{
uint256 length = assetIds.length;
if (length != tokenAddresses.length) {
revert ArrayLengthMismatch();
}
for (uint256 i = 0; i < length;) {
tokens[assetIds[i]] = tokenAddresses[i];
unchecked {
++i;
}
}
}
/**
* @notice Function to update the fee per byte value
* @dev Only callable by governance
* @param newFeePerByte New fee per byte value
*/
function updateFeePerByte(uint256 newFeePerByte) external onlyRole(DEFAULT_ADMIN_ROLE) {
feePerByte = newFeePerByte;
}
/**
* @notice Function to update the fee recipient
* @dev Only callable by governance
* @param newFeeRecipient New fee recipient address
*/
function updateFeeRecipient(address newFeeRecipient) external onlyRole(DEFAULT_ADMIN_ROLE) {
// slither-disable-next-line missing-zero-check
feeRecipient = newFeeRecipient;
}
/**
* @notice Function to withdraw fees to the fee recipient
* @dev Callable by anyone because all fees are always sent to the recipient
*/
function withdrawFees() external {
uint256 fee = fees;
delete fees;
// slither-disable-next-line low-level-calls
(bool success,) = feeRecipient.call{value: fee}("");
if (!success) {
revert WithdrawFailed();
}
}
/**
* @notice Takes an arbitrary message and its proof of inclusion, verifies and executes it (if valid)
* @dev This function is used for passing arbitrary data from Avail to Ethereum
* @param message Message that is used to reconstruct the bridge leaf
* @param input Merkle tree proof of inclusion for the bridge leaf
*/
function receiveMessage(Message calldata message, MerkleProofInput calldata input)
external
whenNotPaused
onlySupportedDomain(message.originDomain, message.destinationDomain)
nonReentrant
{
if (message.messageType != MESSAGE_TX_PREFIX) {
revert InvalidMessage();
}
_checkBridgeLeaf(message, input);
// downcast SCALE-encoded bytes to an Ethereum address
address dest = address(bytes20(message.to));
IMessageReceiver(dest).onAvailMessage(message.from, message.data);
emit MessageReceived(message.from, dest, message.messageId);
}
/**
* @notice Takes an AVAIL transfer message and its proof of inclusion, verifies and executes it (if valid)
* @dev This function is used for AVAIL transfers from Avail to Ethereum
* @param message Message that is used to reconstruct the bridge leaf
* @param input Merkle tree proof of inclusion for the bridge leaf
*/
function receiveAVAIL(Message calldata message, MerkleProofInput calldata input)
external
whenNotPaused
onlySupportedDomain(message.originDomain, message.destinationDomain)
onlyTokenTransfer(message.messageType)
{
(bytes32 assetId, uint256 value) = abi.decode(message.data, (bytes32, uint256));
if (assetId != 0x0) {
revert InvalidAssetId();
}
_checkBridgeLeaf(message, input);
// downcast SCALE-encoded bytes to an Ethereum address
address dest = address(bytes20(message.to));
emit MessageReceived(message.from, dest, message.messageId);
avail.mint(dest, value);
}
/**
* @notice Takes an ETH transfer message and its proof of inclusion, verifies and executes it (if valid)
* @dev This function is used for ETH transfers from Avail to Ethereum
* @param message Message that is used to reconstruct the bridge leaf
* @param input Merkle tree proof of inclusion for the bridge leaf
*/
function receiveETH(Message calldata message, MerkleProofInput calldata input)
external
whenNotPaused
onlySupportedDomain(message.originDomain, message.destinationDomain)
onlyTokenTransfer(message.messageType)
nonReentrant
{
(bytes32 assetId, uint256 value) = abi.decode(message.data, (bytes32, uint256));
if (assetId != ETH_ASSET_ID) {
revert InvalidAssetId();
}
_checkBridgeLeaf(message, input);
// downcast SCALE-encoded bytes to an Ethereum address
address dest = address(bytes20(message.to));
emit MessageReceived(message.from, dest, message.messageId);
// slither-disable-next-line arbitrary-send-eth,missing-zero-check,low-level-calls
(bool success,) = dest.call{value: value}("");
if (!success) {
revert UnlockFailed();
}
}
/**
* @notice Takes an ERC20 transfer message and its proof of inclusion, verifies and executes it (if valid)
* @dev This function is used for ERC20 transfers from Avail to Ethereum
* @param message Message that is used to reconstruct the bridge leaf
* @param input Merkle tree proof of inclusion for the bridge leaf
*/
function receiveERC20(Message calldata message, MerkleProofInput calldata input)
external
whenNotPaused
onlySupportedDomain(message.originDomain, message.destinationDomain)
onlyTokenTransfer(message.messageType)
nonReentrant
{
(bytes32 assetId, uint256 value) = abi.decode(message.data, (bytes32, uint256));
address token = tokens[assetId];
if (token == address(0)) {
revert InvalidAssetId();
}
_checkBridgeLeaf(message, input);
// downcast SCALE-encoded bytes to an Ethereum address
address dest = address(bytes20(message.to));
emit MessageReceived(message.from, dest, message.messageId);
IERC20(token).safeTransfer(dest, value);
}
/**
* @notice Emits a corresponding arbitrary messag event on Avail
* @dev This function is used for passing arbitrary data from Ethereum to Avail
* @param recipient Recipient of the message on Avail
* @param data Data to send
*/
function sendMessage(bytes32 recipient, bytes calldata data) external payable whenNotPaused {
uint256 length = data.length;
if (length >= MAX_DATA_LENGTH) {
revert ExceedsMaxDataLength();
}
// ensure that fee is above minimum amount
if (msg.value < getFee(length)) {
revert FeeTooLow();
}
uint256 id;
unchecked {
id = messageId++;
}
fees += msg.value;
Message memory message = Message(
MESSAGE_TX_PREFIX, bytes32(bytes20(msg.sender)), recipient, ETH_DOMAIN, AVAIL_DOMAIN, data, uint64(id)
);
// store message hash to be retrieved later by our light client
isSent[id] = keccak256(abi.encode(message));
emit MessageSent(msg.sender, recipient, id);
}
/**
* @notice Burns amount worth of WAVAIL tokens and bridges it to the specified recipient on Avail
* @dev This function is used for WAVAIL transfers from Ethereum to Avail
* @param recipient Recipient of the AVAIL tokens on Avail
* @param amount Amount of AVAIL tokens to bridge
*/
function sendAVAIL(bytes32 recipient, uint256 amount) external whenNotPaused checkDestAmt(recipient, amount) {
uint256 id;
unchecked {
id = messageId++;
}
Message memory message = Message(
TOKEN_TX_PREFIX,
bytes32(bytes20(msg.sender)),
recipient,
ETH_DOMAIN,
AVAIL_DOMAIN,
abi.encode(bytes32(0), amount),
uint64(id)
);
// store message hash to be retrieved later by our light client
isSent[id] = keccak256(abi.encode(message));
emit MessageSent(msg.sender, recipient, id);
avail.burn(msg.sender, amount);
}
/**
* @notice Bridges ETH to the specified recipient on Avail
* @dev This function is used for ETH transfers from Ethereum to Avail
* @param recipient Recipient of the ETH on Avail
*/
function sendETH(bytes32 recipient) external payable whenNotPaused checkDestAmt(recipient, msg.value) {
uint256 id;
unchecked {
id = messageId++;
}
Message memory message = Message(
TOKEN_TX_PREFIX,
bytes32(bytes20(msg.sender)),
recipient,
ETH_DOMAIN,
AVAIL_DOMAIN,
abi.encode(ETH_ASSET_ID, msg.value),
uint64(id)
);
// store message hash to be retrieved later by our light client
isSent[id] = keccak256(abi.encode(message));
emit MessageSent(msg.sender, recipient, id);
}
/**
* @notice Bridges ERC20 tokens to the specified recipient on Avail
* @dev This function is used for ERC20 transfers from Ethereum to Avail
* @param assetId Asset ID of the ERC20 token
* @param recipient Recipient of the asset on Avail
* @param amount Amount of ERC20 tokens to bridge
*/
function sendERC20(bytes32 assetId, bytes32 recipient, uint256 amount)
external
whenNotPaused
checkDestAmt(recipient, amount)
{
address token = tokens[assetId];
if (token == address(0)) {
revert InvalidAssetId();
}
uint256 id;
unchecked {
id = messageId++;
}
Message memory message = Message(
TOKEN_TX_PREFIX,
bytes32(bytes20(msg.sender)),
recipient,
ETH_DOMAIN,
AVAIL_DOMAIN,
abi.encode(assetId, amount),
uint64(id)
);
// store message hash to be retrieved later by our light client
isSent[id] = keccak256(abi.encode(message));
emit MessageSent(msg.sender, recipient, id);
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
}
/**
* @notice Takes a Merkle tree proof of inclusion for a blob leaf and verifies it
* @dev This function is used for data attestation on Ethereum
* @param input Merkle tree proof of inclusion for the blob leaf
* @return bool Returns true if the blob leaf is valid, else false
*/
function verifyBlobLeaf(MerkleProofInput calldata input) external view returns (bool) {
if (input.blobRoot == 0x0) {
revert BlobRootEmpty();
}
_checkDataRoot(input);
// leaf must be keccak(blob)
// we don't need to check that the leaf is non-zero because we hash the pre-image here
return input.leafProof.verify(input.blobRoot, input.leafIndex, keccak256(abi.encode(input.leaf)));
}
/**
* @notice Takes a Merkle tree proof of inclusion for a bridge leaf and verifies it
* @dev This function does not validate that the leaf itself is valid, only that it's included
* @param input Merkle tree proof of inclusion for the bridge leaf
* @return bool Returns true if the bridge leaf is valid, else false
*/
function verifyBridgeLeaf(MerkleProofInput calldata input) public view returns (bool) {
if (input.bridgeRoot == 0x0) {
revert BridgeRootEmpty();
}
_checkDataRoot(input);
// leaf must be keccak(message)
// we don't need to check that the leaf is non-zero because we check that the root is non-zero
return input.leafProof.verify(input.bridgeRoot, input.leafIndex, input.leaf);
}
/**
* @notice Returns the minimum fee for a given message length
* @param length Length of the message (in bytes)
* @return uint256 The minimum fee
*/
function getFee(uint256 length) public view returns (uint256) {
return length * feePerByte;
}
/**
* @notice Takes a message and its proof of inclusion, verifies and marks it as spent (if valid)
* @dev This function is used for verifying a message and marking it as spent (if valid)
* @param message Message that is used to reconstruct the bridge leaf
* @param input Merkle tree proof of inclusion for the bridge leaf
*/
function _checkBridgeLeaf(Message calldata message, MerkleProofInput calldata input) private {
bytes32 leaf = keccak256(abi.encode(message));
if (isBridged[leaf]) {
revert AlreadyBridged();
}
// validate that the leaf being proved is indeed the message hash!
if (input.leaf != leaf) {
revert InvalidLeaf();
}
// check proof of inclusion
if (!verifyBridgeLeaf(input)) {
revert InvalidMerkleProof();
}
// mark as spent
isBridged[leaf] = true;
}
/**
* @notice Takes a Merkle proof of inclusion, and verifies it
* @dev This function is used for verifying a Merkle proof of inclusion for a data root
* @param input Merkle tree proof of inclusion for the data root
*/
function _checkDataRoot(MerkleProofInput calldata input) private view {
bytes32 dataRootCommitment = vectorx.dataRootCommitments(input.rangeHash);
if (dataRootCommitment == 0x0) {
revert DataRootCommitmentEmpty();
}
// we construct the data root here internally, it is not possible to create an invalid data root that is
// also part of the commitment tree
if (
!input.dataRootProof.verify(
dataRootCommitment, input.dataRootIndex, keccak256(abi.encode(input.blobRoot, input.bridgeRoot))
)
) {
revert InvalidDataRootProof();
}
}
}
```
### Wrapped AVAIL specification
The `Wrapped Avail` is a standard ERC20 token based on the open-source OpenZeppelin implementation with EIP-2612 `permit()` functionalities to allow for signature-based approvals. The `WAVAIL` has exposed `mint()` and `burn()` functions, the `mint()` function is only callable by the `AvailBridge` contract mentioned above, the `burn()` function allows any `WAVAIL` holder to burn their tokens and specify a destination, the contract does not validate the destination, as it’s not technically feasible or worthwhile to do so.
```solidity
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.23;
import {ERC20, ERC20Permit} from "lib/openzeppelin-contracts/contracts/token/ERC20/extensions/ERC20Permit.sol";
import {IWrappedAvail} from "src/interfaces/IWrappedAvail.sol";
/**
* @author @QEDK (Avail)
* @title WrappedAvail
* @notice An Avail token implementation for Ethereum
* @custom:security security@availproject.org
*/
contract WrappedAvail is ERC20Permit, IWrappedAvail {
address public immutable bridge;
error OnlyAvailBridge();
constructor(address _bridge) ERC20Permit("Wrapped Avail") ERC20("WAVAIL", "Wrapped Avail") {
// slither-disable-next-line missing-zero-check
bridge = _bridge;
}
modifier onlyAvailBridge() {
if (msg.sender != bridge) {
revert OnlyAvailBridge();
}
_;
}
function mint(address destination, uint256 amount) external onlyAvailBridge {
_mint(destination, amount);
}
function burn(address from, uint256 amount) external onlyAvailBridge {
_burn(from, amount);
}
}
```
### Merkle tree proof of inclusion security assumptions
We need five different kind of guarantees from the bridge contract:
- Whether a message leaf is verifiably part of the bridging tree $(1.0)$
- We enforce this by forcing a reveal of the entire leaf in a commit-reveal scheme, where the bridge contract enforces the *reveal* behaviour $(1.1)$
- Since it is virtually impossible to get a pre-image of an internal node that conforms to a message leaf hash, we can assume this construct to be secure
- Whether a blob leaf is verifiably part of the blob tree $(2.0)$
- It is important to remember that `blob` leaves are hashed twice, like $keccak(keccak(blob))$
- This distinction is important because:
1. We want to prove inclusion of blobs without revealing the blobs, otherwise, we end up overpaying L1 for data attestation, where we want to prove data inclusion on Avail.
2. It should not be possible to pass an internal node as a blob leaf.
- Therefore, instead of forcing a *reveal* of the blob itself, we make the user reveal a commitment of the blob. $(2.1)$
- Since it is virtually impossible to get a pre-image of a leaf or internal node such that it conforms to a submitted blob’s commitment, we can assume this construct to be secure
- Whether a message root is verifiably part of the data root $(3.0)$
- We first construct a verifiable message root per $(1.0)$ and force a reveal of the $blobRoot$ to construct the $dataRoot$ $(3.1)$
- Since it is virtually impossible to get a pre-image of a root such that it conforms to the composed root $dataRoot$ we can assume this construct to be secure
- Whether a blob root is verifiably part of the data root $(4.0)$
- We first construct a verifiable blob root per $(2.0)$ and force a reveal of the $bridgeRoot$ to construct the $dataRoot$ $(4.1)$
- Since it is virtually impossible to get a pre-image of a root such that it conforms to the composed root $dataRoot$, we can assume this construct to be secure
- Whether a data root is verifiable part of the data root commitment in `VectorX`
- Given $(3.1)$ and $(4.1)$ we can assume that data root construction is valid.
- Since it is virtually impossible to get a pre-image of an internal node such that it conforms to the composed root $dataRoot$, we can assume this construct to be secure
### Message receiver specification
The `AvailBridge` is designed such that any contract that follows the `MessageReceiver` specification is able to receive messages from Avail through the arbitrary message bridge. For the purposes of usability, we require any contract willing to receive messages from Avail along the lines of this contract, they should override the internal function and replace it with the requisite business logic.
```solidity
// SPDX-License-Identifier: Apache-2.0
pragma solidity ^0.8.22;
import {IMessageReceiver} from "./interfaces/IMessageReceiver.sol";
abstract contract MessageReceiver is IMessageReceiver {
address public availBridge;
error OnlyAvailBridge();
function onAvailMessage(bytes32 from, bytes calldata data) external virtual {
if (msg.sender != availBridge) {
revert OnlyAvailBridge();
}
_onAvailMessage(from, data);
}
function __MessageReceiver_init(address _availBridge) internal virtual {
availBridge = _availBridge;
}
function _onAvailMessage(bytes32 from, bytes calldata data) internal virtual;
}
```
It should be noted that that is simply an available construct, they can choose to simply implement `onAvailMessage()` and the contract will be capable of receiving messages from the bridge.
## Ethereum to Avail bridge specification
Succinct operates a Telepathy bridge that can be used for cross-chain messaging. It can send messages from any supported source chain that uses Ethereum consensus to any target chain that can run Telepathy light client. To support this Avail bridge pallet is implemented.
## Bridge pallet
Telepathy, at its core, is an interoperability protocol for Ethereum that is secured by verifying the signatures of Ethereum validators on-chain. In this section, we will walk through at a high-level how Telepathy enables secure arbitrary messaging between Ethereum and any other chain.
A detailed explanation and overview of the telepathy protocol can be found here: [https://docs.telepathy.xyz/telepathy-protocol/overview](https://docs.telepathy.xyz/telepathy-protocol/overview)
Overview of arbitrary message passing with Telepathy:
%20a00c2aa4937d496ea346d02a6bb119ff/Untitled%201.png)
Implementation of the Avail bridge pallet that reflects the Telepathy client can be found [here](https://github.com/availproject/avail/tree/main/pallets/vector/src).
The main entry point of the Avail bridge pallet is [fulfill_call](https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L397) which updates the state of the Ethereum network based on the provided proof and the input parameters.
There are two types of updates `Step` and `Rotate`.
- Step - To add a new block header to the Avail bridge pallet storage, we must verify that at least two-thirds of the sync committee has provided valid signatures for this block header.
- Rotate - Updates current sync committee.
### Sending messages
The message that is emitted from the smart contract (sent via storage) on the Ethereum network is reflected in the Avail bridge pallet in terms of execution state root.
An example of the emitted message can be found [here](https://www.notion.so/The-Avail-Bridge-Public-Copy-a00c2aa4937d496ea346d02a6bb119ff?pvs=21).
Once the message has been emitted on the Ethereum network, the *operator* using zero-knowledge proves that the header is correct by submitting extrinsic as a `step` (StepFunctionId) call of the [fulfill_call](https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L398) and the `execution state root` gets updated as well as other important fields for the particular `slot`.
The message sent from Avail to the destination chain is submitted as an extrinsic `send_message` that can be found [here](https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L531) and based on the provided parameters validation is done. This message is filtered with [filter_vector_call](https://github.com/availproject/avail/blob/main/runtime/src/filter.rs#L54) and used as data for a data root construction.
### Receiving the message
Avail implementation of the message execution and `execute` extrinsic can be found [here](https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L459).
The function will check all relevant preconditions and execute a message with a given proof that is validated against the bridge pallet execution state root from the storage. The provided proofs will be checked using [patricia-merkle-trie](https://github.com/availproject/avail/tree/main/patricia-merkle-trie) which checks and validates account and storage proofs.
Preconditions to check:
- The message must not be executed more than once.
- The destination domain must be supported.
- The origin domain must be supported.
- The broadcaster's address must be set.
- The source chain must not be frozen.
- The corresponding origin domain must not be frozen.
Once the message has been proven valid, it will be executed and marked as *`ExecutionSucceeded`*.
## Links related to the bridge on the Avail node side
**Avail Bridge pallet of TelepathyX:**
[https://github.com/availproject/avail/tree/data-root/pallets/vector/src](https://github.com/availproject/avail/tree/main/pallets/vector/src)
**The main entry point of the Vector pallet for Step and Rotate functions: fulfill_call**
[https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L398](https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L398)
**Message execution:**
[https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L459](https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L459)
**Merkle-Patricia library for message proofs:**
[https://github.com/availproject/avail/tree/main/patricia-merkle-trie](https://github.com/availproject/avail/tree/main/patricia-merkle-trie)
**Send message data on Avail:**
[https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L568](https://github.com/availproject/avail/blob/main/pallets/vector/src/lib.rs#L568)
**Merkle proof/data root of the submitted transactions:**
[https://github.com/availproject/avail/blob/main/base/src/calls_proof.rs](https://github.com/availproject/avail/blob/main/base/src/calls_proof.rs)
[https://github.com/availproject/avail/tree/main/base/src/data_root](https://github.com/availproject/avail/tree/main/base/src/data_root)
[https://github.com/availproject/avail/blob/main/runtime/src/filter.rs](https://github.com/availproject/avail/blob/main/runtime/src/filter.rs)
**Query data proof rpc call kate_queryDataProof:**
[https://github.com/availproject/avail/blob/main/rpc/kate-rpc/src/lib.rs#L58](https://github.com/availproject/avail/blob/main/rpc/kate-rpc/src/lib.rs#L58)
**Avail core library that constructs proof response:**
[https://github.com/availproject/avail-core/blob/main/core/src/data_proof/message.rs](https://github.com/availproject/avail-core/blob/main/core/src/data_proof/message.rs)
[https://github.com/availproject/avail-core/blob/main/core/src/data_proof.rs](https://github.com/availproject/avail-core/blob/main/core/src/data_proof.rs)