This document aims to provide a high-level overview of Fuel's token bridge and its current operation.
Before delving into the details, let's understand a few key concepts that will be referenced throughout this document:
- Layer 1 (L1): An EVM blockchain, namely Ethereum.
- Layer 2 (L2): The Fuel blockchain will sometimes be referred as the Layer 2 or L2.
- Fuel block: a block of the Fuel blockchain, generated by sequencers. It contains transactions, but also messages, originating from L1 with L2 destination and viceversa.
- Fuel root block: the last block of a Fuel epoch.
- Fuel epoch: A group of Fuel blocks, packed together and identified by the last block of the epoch.
- (Incoming / Outgoing) messages: depending on the context, messages will be referred as "incoming" or "outgoing". A L1 -> L2 message is an outgoing message from the L1 perspective, and an incoming message from the L2 perspective. A L2 -> L1 message inverts those tags.
- Relayed messages: Messages that are generated in the L2 (Fuel) blockchain that must be sent to L1 entities.
- Fuel sequencers: Entities that process L2 transactions and keep the L2 blockchain updated. These entities are akin to Bitcoin miners or ETH 2.0 validators.
- L1 Commits: Transactions on the L1 that start the finalization process of a Fuel epoch.
- Finality: State of commits by which the commit cannot be changed, it is considered honest and immutable - final. When an epoch is committed, a clock starts. If the commit is correct, time will pass and the epoch will be final - it cannot be reverted or changed. If the commit is not correct, an honest party can challenge it.
- Block comitter: entity responsible for listening to finished epochs from the Fuel Blockchain and comitting these epochs to the
FuelChainState
contract. FuelChainState
contract: L1 Smart contract that holds epoch information.FuelMessagePortal
contract: L1 Smart contract that is able to validate relay messages from Fuel Blocks that have been committed to the state contract, and relays those messages to the corresponding entities in L1- Layer 1 Bridge smart contract: L1 smart contract that holds L1 tokens. Upon user deposits, it generates an message for the L2 counterpart to mint the associated L2 token.
- Layer 2 Bridge smart contract: L2 (Fuel) smart contract that mints L2 tokens when receiving messsages from the L1 counterpart. Similarly, burns L2 tokens and generates a withdrawal message for the L1 contract to release L1 tokens to their rightful owner.
- Deposit: User action by which some L1 tokens are locked in the L1 bridge smart contracts, minting the same amount in the L2 chain.
- Withdrawal: User action by which some L2 tokens are burnt in the L2 bridge smart contracts, releasing the burnt amount in the L1 chain.
Fuel 's bridge system is built on a message protocol that allows the sending (and receiving) of messages between entities located in two different blockchains, namely the L1 (Ethereum) and L2 (Fuel blockchain). The system features sending messages in both directions (L1 to L2, and L2 to L1), though the mechanisms involved for each direction are different and almost independent:
- A message that goes from L1 to L2 originates with an Ethereum event, to which Fuel sequencers will be listening. The event that creates this message will be parsed and included as an UTXO in the Fuel chain.
- A message that goes from L2 to L1 originates with a Fuel receipt. Receipts are bound with Fuel block headers. The block committer will push witnesses (Merkle roots) to Ethereum, so that these messages can be trustlessly unwrapped and sent via Merkle proofs to their recipients.
It can be derived that; if the entities receiving these messages are capable of interpreting them, some actions can be executed.
From here on, you will read first the logic involved in the L1 to L2 message passing, and viceversa after.
The Message Portal contains a sendMessage
function that can be called by any entity on the L1 blockchain. This function will emit an event MessageSent
to be picked up by Fuel's sequencers, optionally containing an ETH value that will be depositted in the contract, and a data payload. The sequencers will include said message in the following blocks of the L2 blockchain, by adding an UTXO that reflects the original message. Messages will be reflected in the block header's inbox.
The MessageSent
event emitted on the Ethereum chain and its counterpart UTXO MessageCoin
on the Fuel chain hold, among other fields, a value
(amount of ETH that is deposited), a payload data
and an ID recipient
that can spend this message in the L2.
A summarized user journey can be described as follows:
- An EVM address (EOA or smart contract) calls
sendMessage
in theFuelMessagePortal
. This message might have attached some ETH. An event containing therecipient
of the message in the L2, the attachedvalue
and adata
payload will be emitted in this call - Fuel sequencers will listen for these events and include them as an UTXO during block generation in the L2.
- The recipient will be able to make use of the UTXO as part of the normal L2 activities.
The following figure contains a view of all the components involved in the L1 outgoing messages
erDiagram
"FuelMessagePortal.sol" {
function sendMessage
event MessageSent
}
"FuelMessagePortal.sol" ||--|| "ETH Blockchain" : "host"
"(user) ETH depositor" }|--|| "FuelMessagePortal.sol" : "sendMessage(to: fuel wallet)"
"FuelERC20Gateway.sol" {
function deposit
}
"FuelERC20Gateway.sol" ||--|| "FuelMessagePortal.sol" : "sendMessage(to: bridge predicate)"
"Fuel Sequencers" }|--|| "FuelMessagePortal.sol" : "Listens MessageSent"
"Fuel Sequencers" }|--|| "Fuel Blockchain" : "Include MessageSent"
"(user) ERC20 depositor" }|--|| "FuelERC20Gateway.sol" : "deposit"
"erc20bridge.sw" ||--|| "Fuel Blockchain" : "mints assets"
"bridge predicate" ||--|| "Fuel Blockchain" : "host"
"erc20bridge.sw" {
function process_message
}
"(user) ERC20 depositor" }|--|| "bridge predicate" : "run"
"bridge predicate" ||--|| "erc20bridge.sw" : "relay MessageSent data"
Since MessageSent
events and their spawned MessageCoin
UTXOs can carry a value, an ETH deposit consists merely of the action of calling sendMessage
on the L1 with some attached ETH. The recipient in the L2 will see its balance updated by means of the inclusion of the MessageCoin
that reflects this value in the L2.
sequenceDiagram
participant user as User
box ETH
participant eth as L1 Chain
participant portal as FuelMessagePortal.sol
end
box FUEL
participant sequencers as Fuel Sequencers
participant fuel as L2 Chain
end
user ->>+ eth: send tx with ETH
eth ->> portal: sendMessage()
note over eth,portal: {to: fuel address}
portal --> eth: emit MessageSent event
eth ->>- user: Confirm tx
loop MessageSent subscription
sequencers ->> portal: get MessageSent events
portal ->> sequencers: MessageSent events
sequencers ->> fuel: Include events
end
note over sequencers,fuel: The deposit is reflected as an UTXO<br/>owned by the recipient of the<br> MessageSent event in ETH
A deposit operation is an use case of the message passing system from L1 entities to L2 entities. The user will send a transaction to deposit an asset in a L1 bridge contract FuelERC20Gateway
, which will in turn call sendMessage
and emit a MessageSent
event through the FuelMessagePortal
to be picked up by the Fuel blockchain 's sequencer. The recipient of this message shall be the bridge predicate
(a L2 entity that posseses an ID), whose role will be explained further ahead. The payload of the message will just reflect the fact that the FuelERC20Gateway
has received a deposit of a L1 token, with a given amount and a desired recipient. Sequencers will include this message as part of an UTXO that can be spent by the bridge predicate
. The predicate
holds these UTXOs until any other entity (which can be the users themselves, or another "relayer" entity) executes the predicate 's logic, which has the responsibility for running validation logic before sending the data to the bridge contract on the L2, erc20bridge.sw
, by calling process_message
. Once the UTXO is "spent" (delivered), this last contract will have the capability of processing the payload inside the message, validating that their sender is the L1 bridge contract FuelERC20Gateway
, and then proceeding to mint an equivalent asset and amount in the Fuel blockchain to the one originally deposited in the L1. Then, the minted assets become available to the user in the L2.
sequenceDiagram
participant user as User
box ETH
participant eth as L1 Chain
participant gateway as FuelERC20Gateway.sol
participant portal as FuelMessagePortal.sol
end
box FUEL
participant sequencers as Fuel Sequencers
participant fuel as L2 Chain
participant predicate as Bridge predicate
participant bridge as Bridge contract
end
user ->>+ eth: send deposit tx
eth ->> gateway: deposit()
gateway ->> portal: sendMessage
note over gateway,portal: {to: predicate, payload: deposit data}
portal --> eth: emit MessageSent event
eth ->>- user: Confirm tx
loop MessageSent subscription
sequencers ->> portal: get MessageSent events
portal ->> sequencers: MessageSent events
sequencers ->> fuel: Include events
fuel ->> predicate: Assign MessageCoin UTXO
end
user ->>+ fuel: (cont.) send deposit tx
note over sequencers,fuel: The transaction sits in the pool<br/>until included and executed
alt Validation happy path
fuel ->> predicate: Run predicate
predicate ->> bridge: call process_message
bridge ->> fuel: mint asset
note over bridge,fuel: The asset is sent as an Coin UTXO to the user
else Validation failed
bridge ->> fuel: trigger refund
end
fuel ->>- user: tx result
You can follow the actual implementation of the message processing flow via:
- L1 FuelERC20Gateway 's
deposit
function (check last version of the contract) - FuelMessagePortal 's
sendMessage
function (check last version) - L2 Bridge contract 's
process_message
function - L2 predicate predicate that holds the UTXOs meant to be relayed to the bridge contract. Runs some pre-validation logic before relaying the messages to the bridge contract.
Any user (or contract) on the L2 can trigger transactions that generate receipts. Among these receipts, it is possible to include the MessageOut
receipt, carrying among other things, a sender
ID from the Fuel chain, a recipient
address of the L1 chain, an amount
of the base currency used in the L1, and a data
payload. Each Fuel block header contains a merkle root built from the MessageOut
receipts, making it trivial to build a merkle proof for the inclusion of specific MessageOut
payloads in a Fuel block.
Fuel blocks are packed and committed together by the mechanism described in the section Block committing
in epochs. A Fuel epoch will be committed at the Chain State contract with the last block of the epoch, namely the Fuel root block
. This block features another Merkle root
that commits to a tree consisting of the block hashes of the epoch. Again, the hash of this root block is derived, among other elements, from this root.
Once the committed epoch has finalized, an user in the L1 blockchain can prove that the original message was included in the finalized epoch:
- First, it is needed to prove that a certain block (identified by its block hash) exists in the committed epoch. This is done with a
Merkle proof
. - Once proven that the block exists, by the same mechanism, it is possible to prove that the message exists as part of the block.
An user can request proofs of inclusion of both the block inside the epoch, and the message inside the block, to the L2, then attach those proofs on a call to Message Portal 's relayMessage
. The portal will check that the proofs are correct and the finalization status of the epoch, then proceed to unpack the payload of the message, that should contain execution instructions.
Find below a figure describing the general relationships between all the systems involved in this process.
erDiagram
"FuelMessagePortal.sol" {
function relayMessage
}
"FuelChainState.sol" {
function commit
function finalized
}
"FuelMessagePortal.sol" ||--|| "ETH Blockchain" : "host"
"FuelMessagePortal.sol" ||--|| "FuelChainState.sol" : "checks finalized()"
"Block Committer" ||--|| "FuelChainState.sol" : "commit() blocks"
"Block Committer" ||--|| "Fuel Blockchain" : "Reads blocks"
"FuelChainState.sol" ||--|| "ETH Blockchain" : "host"
"(user) withdrawer" }|--|| "FuelMessagePortal.sol" : "relayMessage(message,block data)"
"(user) withdrawer" }|--|| "Fuel Blockchain" : "burn eth"
"(user) withdrawer" }|--|| "erc20bridge.sw" : "burn assets"
"Fuel Sequencers" }|--|| "Fuel Blockchain" : "Build blocks with txs"
"erc20bridge.sw" ||--|| "Fuel Blockchain" : "host"
"erc20bridge.sw" {
function withdraw
function claim_refund
}
The Fuel blockchain aims to be scalable, and thus is able to generate blocks at a faster rate than other blockchains, without compromising on its security. It does so by inheriting the security of the L1 it is anchored to. The mechanism allowing this is an hybrid optimistic-ZK rollup approach. Blocks generated in the Fuel blockchain are grouped in epochs (e.g. an epoch can group together 10800 blocks), and epochs are identified and summarized in the last block of said epoch. This block can be committed to the L1 blockchain with a transaction to the FuelChainState
contract, and will be accepted optimistically (i.e. the commit is considered valid unless proven otherwise - validation of the blocks, as well as decentralization of the entities that are able to commit blocks, are not considered in the scope of this document). The commit will be registered with the timestamp of the L1 blockchain. Then, once a certain time period has passed, the commit (along with the block) will be considered final. This finalization enables entities in the L1 chain to execute and relay messages originating from the L2 to their L1 recipients.
sequenceDiagram
participant user as User
box FUEL
participant fuel as L2 Chain
participant sequencers as Fuel Sequencers
end
participant committer as Block committer
box ETH
participant eth as L1 Chain
participant state as FuelChainState.sol
participant observer
end
loop Block creation
user ->> fuel: send txs with<br/>MessageOut receipts
sequencers ->> fuel: build block with user txs
end
loop Block collection
committer ->> fuel: get new blocks
fuel ->> committer: send new blocks
alt if new epoch
committer ->> eth: send tx
eth ->> state: commit(last_block_header_root)
eth ->> committer: tx confirmation
end
end
observer ->> state: finalized(block_header_root)
note over state,observer: Once enough time passes, the root of the<br>block header that was committed<br>is considered valid. This marks the<br>finality of the Fuel Chain
state ->> observer: [false | true]
An ETH withdrawal can be accomplished by sending a transaction from a Fuel entity that generates a MessageOut
receipt, with the desired Ethereum address as recipient, and the withdrawn value coded in the amount
. After the transaction is picked up by the L2 network and included in one of its blocks, eventually, a block committer
shall update the Chain State contract by committing the last epoch of the Fuel blockchain. Once enough time has passed, the epoch will be considered valid and the user will be able to call Message Portal 's relayMessage
with the appropiate Merkle proofs that witness the activity of the L2 chain. This way, the user can prove the existence of a MessageOut
receipt in with these proofs, and the ETH that was originally locked in the portal contract can be finally released to the recipient
of the unpacked MessageOut
specified in the call.
A simplified sequencer diagram shows the process below:
sequenceDiagram
participant user as User
box FUEL
participant fuel as L2 Chain
end
participant committer as Block committer
box ETH
participant state as FuelChainState.sol
participant portal as FuelMessagePortal.sol
end
user ->>+ fuel: send txs with<br/>MessageOut receipt<br>{recipient(evm addr of the user), amount(eth)}
fuel ->> user: confirm tx
loop Block collection
committer ->> fuel: get epoch
fuel ->> committer: send epoch
committer ->> state: commit
end
fuel -->- user: Fuel block was committed<br>in FuelChainState
note over state: Finalization time passes
user ->> fuel: get proofs for tx, block, receipt, etc
fuel ->> user: send proofs
user ->>+ portal: call relayMessage() with proofs
portal ->> state: get involved commits
state ->> portal: send involved commits
portal ->> portal: validate proofs
portal ->>- user: release ETH
An ERC20 withdrawal is an example implementation of message passing from L2 entities to L1 entities.
The user will start the process by sending a transaction towards the L2 Bridge Contract 's withdraw
function, along with the tokens that need to be withdrawn. This function will burn the tokens attached to the transaction and generate a MessageOut
receipt directed to the L1 Bridge Contract (the recipient of the MessageOut
will encode the EVM address of the gateway). The data
payload of this receipt will be the ABI-encoded call to the L1 bridge contract 's finalizeWithdrawal
. Eventually, the block containing this receipt will be committed through the block committer, becoming available to relay through the Message Portal.
Once the transaction has been included in the Fuel blockchain, the user will need to await two events (just as with the ETH withdrawal):
- First, the Fuel blockchain will need to close the epoch. The epoch will be committed to the L1 blockchain in the Chain State contract when a block committer uploads the block header 's root through the
commit
function. - Then, once the epoch has been committed, it is needed to wait for its finalization
TIME_TO_FINALIZE
.
After finality has been reached for the epoch that includes the block where the withdrawal transaction was signaled, the user can craft the proofs of inclusion of both the message in the Fuel block, and the Fuel block in the Fuel epoch. The user will send a transaction call to the Message Portal 's relayMessage
function that will check the attached proofs and the finalization status of the epoch that the message belongs to, then proceed to unpack the payload of said message, that should contain an execution instruction to call finalizeWithdrawal
in the L1 Bridge Contract. The gateway contract will validate the call, finally releasing the tokens to the recipient.
sequenceDiagram
participant user as User
box FUEL
participant fuel as L2 Chain
participant bridge as L2 Bridge Contract
end
participant committer as Block committer
box ETH
participant state as FuelChainState.sol
participant portal as FuelMessagePortal.sol
participant gateway as FuelERC20Gateway.sol
end
user ->>+ fuel: send tx with call to L2 Bridge<br>withdraw function
fuel ->> bridge: call withdraw()
bridge ->> fuel: Burn tokens receipt
bridge ->> fuel: MessageOut receipt<br>to FuelERC20Gateway.sol<br>data abi-encoded call of<br>finalizeWithdrawal()
fuel ->> user: confirm tx
loop Block collection
committer ->> fuel: get epoch
fuel ->> committer: send epoch
committer ->> state: commit
end
fuel -->- user: Fuel block was committed<br>in FuelChainState
note over state: Finalization time passes
user ->> fuel: get proofs for tx, block, receipt, etc
fuel ->> user: send proofs
user ->>+ portal: call relayMessage() with proofs
portal ->> state: get involved commits
state ->> portal: send involved commits
portal ->> portal: validate proofs
portal ->>+ gateway: finalizeWithdrawal()
gateway ->> portal: check<br>sender == L2 Bridge Contract
gateway ->> user: transfer tokens
gateway ->>- portal: return success
portal ->>- user: return success
You can follow the implementation of this flow via:
- bridge_fungible_token.sw 's
withdraw
function - FuelChainState.sol 's
commit
andfinalized
functions - Message Portal 's
relayMessage
function - FuelERC20Gateway.sol 's
finalizeWithdrawal
One of Fuel 's key design principles is to minimize execution costs and minimize state growth. One of the design decisions furthering that goal is the use of 64 bits (u64
) to code the balances that are bridged from L1 to L2. The most popular token standard, ERC20
, codes the amounts with 256 bits
. If the amount transferred from L1 to L2 surpasses the capacity of Fuel to mint the L2 counterpart, a refund
message will be generated, that will allow the user to retrieve the originally deposited amount in L1. We foresee this to be an extreme case, as there are currently no widely used tokens that surpass 2^64
units of supply.
Additionally, it is a de-facto practice in the space to use 18 decimals to represent the token (in itself, this excess of precision contributes to making the amounts in an ERC20 transfers bloated), whereas in Fuel, 9 decimals are used to represent amounts under the unit. This means that there is a loss of precision when bridging L1 amounts. This loss of precision is nullified by enforcing that all deposits made in the L1 do not incur the loss of dust amounts. For example, when depositing ETH, which has 18 decimals of precision, a deposit of 1.0000000010 ETH is allowed, whereas 1.0000000011 would lose 0.0000000001, and therefore such deposit would be rejected and reverted.
DApps that enable bridge operations should observe this limitation and truncate the amounts accordingly to avoid reverted transactions.
SPoF stands for Single Points of Failure
When it comes to using the token bridge and the underlying message exchange system between Ethereum and Fuel, there are some things to consider.
ERC20 that do not adhere fully to the spec might not be compatible or present major flaws when paired with the bridge. Tokens that present the following characteristics:
- Rebasing balances
- Fee on transfer
- Hyper supply (> 2^64 units of total supply in circulation) or hyper precision (high decimal counts)
might observe unexpected behavior. Therefore, it is recommended not to deposit these tokens as there is risk of total loss of funds.
As with many if not all smart contracts publicly available on the Ethereum blockchain, using our smart contracts come with risks of permanent loss of funds. The sources of these risks emanate from:
- Centralization: while we progress towards decentralization, some centralized placeholders run essential components on the whole system. These centralized entities could be compromised and / or produce permanent damages. Refer to the section below on single points of failure for more information.
- Vulnerabilities: the code is audited, but that's not a guarantee that the contracts are safe from these.
- Upgradability: our smart contracts are upgradable and expected to change over time in the short and long term. An upgrade operation always come with risks, as the new code being implemented could introduce new vulnerabilities, be malicious in itself, or brick the smart contract entirely due to a malfunction.
- VM: for each deposit that happens on Ethereum, a mirror asset is created in the L2 following the FuelVM set of rules. All assets minted in the L2 follow the same set of rules are treated equally. A FuelVM malfunction could affect all assets locked in the FuelVM.
A summarized list of risks:
Funds can be permanently stolen if:
- Our contracts receive a malicious or defective upgrade
- Malicious L2 activity is undetected for more than 1 week
Funds can be permantely lost if:
- Our contracts receive a defective upgrade
The chain might be halted as preemptive measure against the risks above. The chain will be halted if
- Malicious activity is detected
- Unintended behaviour of the L2 is detected
This might temporarily lock user funds and cause inconvenience.
As it can be derived from the diagrams above, there are entities performing mission critical tasks:
- Security council and smart contract ownership: the smart contracts will be managed by a security council (by means of a multisig) that enables key administration functions, such as upgrades, granting of roles and permissions for privileged smart contract functions, pausing of the system, etc. It is of the essence that the security council operates correctly, honestly and timely in the management of the smart contracts.
- Fuel Blockchain Sequencer / Validator: The Fuel blockchain's operation is currently a Proof of Authority scheme under which a single private key is able to build new blocks. A compromise on this key could mean the generation of rogue blocks. While it is needed to compromise more than just the private key of the PoA node in order to cause permanent damage, it would halt the network for an unknown amount of time.
- Block committer: The block committer links the activity in the Fuel blockchain by pushing state updates back to Ethereum, where users can use these updates to prove certain aspects of the Fuel blockchain activity (such as withdrawals). If a block committer uploads rogue data to Ethereum, it can enable fraudulent behaviour (for example, uploading withdrawals that never happened). New states uploaded by the committer are timelocked (i.e. they cannot be used to prove L2 activity). If rogue states remain undetected for a time longer than the "finalization window", then a malicious actor can finally propagate the nefarious activity to Ethereum and ultimately extract funds out of our contracts. At that point, the loss is considered final and other avenues must be pursued for the recovery.