Skip to content

Latest commit

 

History

History
534 lines (403 loc) · 34 KB

ARCHITECTURE.md

File metadata and controls

534 lines (403 loc) · 34 KB

Architecture

This document aims to provide a high-level overview of Fuel's token bridge and its current operation.

Concepts

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 vice-versa.
  • 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 messages 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.

Overview

Architecture

Bridge flow

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 vice-versa after.

Message passing from L1 to L2

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 deposited 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 the FuelMessagePortal. This message might have attached some ETH. An event containing the recipient of the message in the L2, the attached value and a data 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"
Loading

L1 (Ethereum) ETH Deposit

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
Loading

L1 (Ethereum) ERC20 Deposit

A deposit operation is a 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 possesses 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
Loading

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.

Message passing from L2 to L1

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
    }
Loading

Block committing

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]
Loading

L2 (Fuel) ETH Withdrawal

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

Loading

L2 (Fuel) ERC20 Withdrawal

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
Loading

You can follow the implementation of this flow via:

Rate Limit Mechanism

The Fuel Bridge uses a rate limiting mechanism as a protection for withdrawals which ensures that until a specific time period passes by only a certain maximum amount of ETH/ERC20 tokens can be withdrawn & the withdrawn limit is reset after each time interval. Fuel's implementation is inspired by the Linea Bridge

There is a small difference between the implementation done by Fuel from Linea's discussed with examples below

solidity

   /**
   Linea Implementation
   * @notice Resets the rate limit amount.
   * @dev If the used amount is higher, it is set to the limit to avoid confusion/issues.
   * @dev Only the RATE_LIMIT_SETTER_ROLE is allowed to execute this function.
   * @dev Emits the LimitAmountChanged event.
   * @dev usedLimitAmountToSet will use the default value of zero if period has expired
   * @param _amount The amount to reset the limit to.
   */
  function resetRateLimitAmount(uint256 _amount) external onlyRole(RATE_LIMIT_SETTER_ROLE) {
    uint256 usedLimitAmountToSet;
    bool amountUsedLoweredToLimit;
    bool usedAmountResetToZero;

    if (currentPeriodEnd < block.timestamp) {
      currentPeriodEnd = block.timestamp + periodInSeconds;
      usedAmountResetToZero = true;
    } else {

      if (_amount < currentPeriodAmountInWei) {
        usedLimitAmountToSet = _amount;
        amountUsedLoweredToLimit = true;
      }
    }

    limitInWei = _amount;

    if (usedAmountResetToZero || amountUsedLoweredToLimit) {
      currentPeriodAmountInWei = usedLimitAmountToSet;
    }

    emit LimitAmountChanged(_msgSender(), _amount, amountUsedLoweredToLimit, usedAmountResetToZero);
  }


   /**
     * Fuel Implementation
     * @notice Resets the rate limit amount.
     * @param _amount The amount to reset the limit to.
     * The difference from the linea implementation is that when currentPeriodEnd >= block.timestamp then if the new rate limit amount is less than the currentPeriodAmount, then currentPeriodAmount is not updated this makes sure that if rate limit is first reduced & then increased within the rate limit duration then any extra amount can't be withdrawn
    */
    function resetRateLimitAmount(uint256 _amount) external onlyRole(SET_RATE_LIMITER_ROLE) {
        // if period has expired then currentPeriodAmount is zero
        if (currentPeriodEnd < block.timestamp) {
            unchecked {
                currentPeriodEnd = block.timestamp + rateLimitDuration;
            }

            currentPeriodAmount = 0;
        }

        limitAmount = _amount;

        emit ResetRateLimit(_amount);
    }

    /**
     * @notice Used to enable/disable rate limit.
     * @param value `true` for enabling rate limit and `false` for disabling.
     * By default rate limit is disabled.
    */
    function updateRateLimitStatus(bool value) external onlyRole(SET_RATE_LIMITER_ROLE) {
        rateLimitEnabled = value;
        emit RateLimitStatusUpdated(value);
    }

Reset Rate Limit Mechanism

Even though resetRateLimitAmount is permissioned, it is still critical in the context of enabling rate limits in the bridge, so it is important to understand how it works.

The account having the SET_RATE_LIMITER_ROLE role can reset the rate limit any time when it is enabled.

If the rate limit is reset before the rateLimitDuration ends, then the rate limit amount is updated (for the ERC20 gateway, the rate limit duration is also updated but only takes effect when the previous rateLimitDuration ends). However, the end timestamp when the current rate limit duration ends and the current withdrawn amount remain unchanged.

If the rate limit is reset after the rateLimitDuration ends, then the rate limit amount is updated along with the end timestamp when the current rate limit duration ends, and the current withdrawn amount is set to 0.

Here are some Do's and Don'ts to keep in mind when calling resetRateLimitAmount.

Do's

  • Simulate the resetRateLimitAmount function call and a withdrawal after that to ensure the rate limit behavior is as desired.
  • Ensure that the rate limit is enabled, if it's not, then the rate limit will have no effect.
  • Communicate with the affected stakeholders in the project, before resetting

Don'ts

  • Avoid calling the resetRateLimitAmount function more than twice for the same asset within the rateLimitDuration as it will affect the user experience and can create confusion among users who want to withdraw.
  • Avoid calling the resetRateLimitAmount function near the end of the current rateLimitDuration, as increasing the limit at that time would allow a higher withdrawal limit for a short period.

Example showcasing the difference in implementation

  • Linea Implementation
Rate Limit Action Current Withdrawal Amount Current Rate Limit
No Update 9 ether 10 ether
Reduced 5 ether 5 ether
Increased 5 ether 10 ether
  • Fuel Implementation
Rate Limit Action Current Withdrawal Amount Current Rate Limit
No Update 9 ether 10 ether
Reduced 9 ether 5 ether
Increased 9 ether 10 ether

Decimals adjustment

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.

This essentially means that if a token with decimals > 9 is bridged to Fuel, then the L2 token minted will have 9 decimals. However, if a token with decimals <= 9 is bridged to Fuel, then the L2 token minted will have the same number of decimals as the L1 token.

Token L1 Token Decimals L2 Token Decimals
USDC 6 6
ETH 18 9
FBTC 8 8

Incompatibilities, risks and SPoF

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 incompatibilities

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.

Risks

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 theft

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 loss

Funds can be permantely lost if:

  • Our contracts receive a defective upgrade

Chain halt

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.

SPoFs

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.