The tools in this repository help you bridge tokens between Tezos layer 1 and Etherlink. You can use them to set up bridging for any Tezos token that is compliant with the FA standard, versions 1.2 or 2. For more information about the FA standards, see Token standards on docs.tezos.com.
The tools also help you submit bridging transactions, both for bridging tokens from Tezos to Etherlink (depositing) and for bridging tokens from Etherlink to Tezos (withdrawing).
For information on how this bridge works, see Bridging FA tokens between Tezos layer 1 and Etherlink in the Etherlink documentation.
You can use the tool to bridge tokens between Tezos Mainnet and Etherlink Mainnet and between Tezos test networks and Etherlink Testnet.
To bridge tokens in this way, you need:
-
An FA-compliant smart contract deployed to Tezos. You can use your own contract or the tool can deploy a sample contract for you.
-
A Tezos wallet with enough tez to pay for the Tezos transaction fees. See Installing and funding a wallet on docs.tezos.com. If you are using a test network, you can find faucets that provide free tez at https://teztnets.com, such as the Ghostnet faucet at https://faucet.ghostnet.teztnets.com/.
-
An EVM-compatible wallet with enough Etherlink XTZ to pay for the Etherlink transaction fees. See Using your wallet in the Etherlink documentation for a list of compatible wallets and information about connecting your wallet to Etherlink. If you are using Etherlink Testnet, you can get free XTZ at the Etherlink faucet.
The bridging process relies on smart contracts that convert tokens to tickets and transfer the tickets between Tezos and Etherlink. These contracts are an implementation of the TZIP-029 standard for bridging between Tezos and Etherlink.
Each FA token needs its own copy of these contracts for you to bridge that token:
-
Ticketer: This Tezos contract stores FA1.2 and FA2.0 tokens and issues tickets that represent those tokens, similar to a wrapped token. The ticket includes the type and number of tokens that it represents and the address of the ticketer contract. For an example, see the
ticketer.mligocontract. -
Token bridge helper: This Tezos contract accepts requests to bridge tokens, uses the ticketer contract to get tickets for them, and sends the tickets to Etherlink. For an example, see
token-bridge-helper.mligo. -
ERC-20 proxy: This Etherlink contract accepts the tickets and mints ERC-20 tokens that are equivalent to the Tezos tokens. This contract implements the ERC20 Proxy deposit interface and withdraw interface. For an example, see
ERC20Proxy.sol.
For information about how these contracts communicate with each other, see bridge configuration.
Follow these steps to set up the tool for local use. You can run the tool directly or build a Docker container that runs it.
-
Install uv if it is not already installed by running this command:
curl -LsSf https://astral.sh/uv/install.sh | sh -
Clone this repository and go into its directory.
-
Install the tool's dependencies by running this command:
uv sync
-
Optional: Rebuild and test the contracts locally as described in compilation and running tests.
All commands are subcommands of the bridge CLI, so you run them as uv run bridge <command>, e.g. uv run bridge bridge_token. Run uv run bridge --help to list them.
-
Clone this repository and go into its directory.
-
Build the Docker image by running this command:
docker build -t etherlink-bridge .
Now you can run commands by prefixing them with docker run --rm etherlink-bridge, such as docker run --rm etherlink-bridge bridge_token.
This tool's bridge_token command deploys the bridging contracts for a single token.
If you have multiple token types, as in FA2 multi-asset contracts, you must run this command once for each type of token to bridge.
The tool also has separate commands for deploying the contracts individually if you want to deploy the contracts one at a time.
Here is an example of the command to deploy the bridging contracts for an FA1.2 token:
uv run bridge bridge_token \
--token-address KT1SekNYSaT3sWp55nhmratovWN4Mvfc6cfQ \
--token-type FA1.2 \
--token-id=0 \
--token-decimals 0 \
--token-symbol "TST12" \
--token-name "Test FA1.2 Token" \
--tezos-private-key ${TEZOS_WALLET_PRIVATE_KEY} \
--tezos-rpc-url "https://rpc.tzkt.io/shadownet" \
--etherlink-private-key ${ETHERLINK_WALLET_PRIVATE_KEY} \
--etherlink-rpc-url "https://node.shadownet.etherlink.com" \
--skip-confirmHere is an example of the command to deploy the bridging contracts for an FA2 token:
uv run bridge bridge_token \
--token-address KT19P1nbGzGnumMfRHcLNuyQUdcuwjpBfsCU \
--token-type FA2 \
--token-id=0 \
--token-decimals 8 \
--token-symbol "TST" \
--token-name "TST Token" \
--tezos-private-key ${TEZOS_WALLET_PRIVATE_KEY} \
--tezos-rpc-url "https://rpc.tzkt.io/shadownet" \
--etherlink-private-key ${ETHERLINK_WALLET_PRIVATE_KEY} \
--etherlink-rpc-url "https://node.shadownet.etherlink.com" \
--skip-confirmThe bridge_token command accepts these arguments:
--token-address: The Tezos address of the token contract, starting withKT1--token-type: The token standard:FA1.2orFA2--token-id: The ID of the token to bridge; for FA1.2 contracts, which have only one token, use0--token-decimals: The number of decimal places used in the token quantity--token-symbol: An alphanumeric symbol for the token--token-name: A name for the token--tezos-private-key: The private key for the Tezos account--tezos-rpc-url: The URL to a Tezos RPC server to send the transactions to; for RPC servers on test networks, see https://teztnets.com--etherlink-private-key: The private key for the EVM-compatible wallet that is connected to Etherlink--etherlink-rpc-url: The URL to the Etherlink RPC server to send the transactions to; see Network information on docs.etherlink.com--skip-confirm: Skip the confirmation step; required if you are running the command via Docker
The output of the command includes the addresses of the contracts and the content of the ticket that they use for bridging. Save this information for use in depositing and withdrawing tokens.
After you have set up bridging for a token, you can bridge tokens from Tezos to Etherlink with the fa_deposit command, as in this example:
uv run bridge fa_deposit \
--token-bridge-helper-address KT1Ejrkzge6GoqUc7rWRT6kQKD5FYueKyjvH \
--amount 77 \
--tezos-private-key ${TEZOS_WALLET_PRIVATE_KEY} \
--tezos-rpc-url "https://rpc.tzkt.io/shadownet" \
--receiver-address 0x7e6f6CCFe485a087F0F819eaBfDBfb1a49b97677 \
--smart-rollup-address sr19fMYrr5C4qqvQqQrDSjtP31GcrWjodzvgThe fa_deposit command takes these parameters:
--token-bridge-helper-address: The address of the token bridge helper contract, from the output of thebridge_tokencommand--amount: The amount of tokens to bridge--tezos-private-key: The private key for the Tezos account that is depositing the tokens--tezos-rpc-url: The URL to a Tezos RPC server to send the transactions to; for RPC servers on test networks, see https://teztnets.com--receiver-address: The Etherlink account address of the account to send the tokens to--smart-rollup-address: The address of the Etherlink Smart Rollup, which issr1Ghq66tYK9y3r8CC1Tf8i8m5nxh8nTvZEffor Mainnet andsr19fMYrr5C4qqvQqQrDSjtP31GcrWjodzvgfor Testnet (Shadownet)
If the deposit transaction is successful, the command returns the hash of the transaction, which you can look up on a Tezos block explorer.
The deposit is not immediately usable on Etherlink: the kernel queues the FA deposit (emitting a QueuedDeposit event), and the ERC-20 tokens are minted only once the deposit is claimed on the Etherlink side. (Native XTZ deposits complete automatically and need no claim.) For whitelisted tokens a Watchtower service submits the claim. Once claimed, you can see the bridged tokens by looking up the ERC-20 proxy contract or your Etherlink account on the Etherlink block explorer.
After you bridge tokens to Etherlink, you can withdraw them back to Tezos with the fa_withdraw command, as in this example:
uv run bridge fa_withdraw \
--erc20-proxy-address 0x3dFF505A2A69e6e0b05fDB71b5F6DDd514fDaF47 \
--amount 1 \
--tezos-side-router-address KT1LdyznoJDzUqsgE8zpM242W1BBi42S8img \
--ticketer-address-bytes 0x01843d2272438cbe9bdc32423c41cb9cfc785e381c00 \
--ticket-content-bytes 0x0707000005090a000000b90502000000b307040100000010636f6e74726163745f616464726573730a000000244b543157633773586a70436b7a59454465727478526339736e776775744658384d6d717607040100000008646563696d616c730a0000000136070401000000046e616d650a0000000a546574686572205553440704010000000673796d626f6c0a000000045553447407040100000008746f6b656e5f69640a00000001300704010000000a746f6b656e5f747970650a00000003464132 \
--receiver-address tz1ekkzEN2LB1cpf7dCaonKt6x9KVd9YVydc \
--etherlink-private-key ${ETHERLINK_WALLET_PRIVATE_KEY} \
--etherlink-rpc-url "https://node.shadownet.etherlink.com"The fa_withdraw command accepts these arguments:
--erc20-proxy-address: The address of the ERC-20 proxy contract on Etherlink--amount: The amount of tokens to withdraw--tezos-side-router-address: The address of the ticketer contract on Tezos or a separate router contract if the token uses a router for bridging--ticketer-address-bytes: The address of the ticketer contract as a series of bytes, which you can get from the output of thebridge_tokencommand or theget_ticketer_paramscommand--ticket-content-bytes: The content of the ticket as a series of bytes, which you can get from the output of thebridge_tokencommand or theget_ticketer_paramscommand--receiver-address: The Tezos address of the account to send the tokens to--etherlink-private-key: The private key for the EVM-compatible wallet that is connected to Etherlink--etherlink-rpc-url: The URL to the Etherlink RPC server to send the transactions to; see Network information on docs.etherlink.com
The output of the command includes the hash of the Etherlink transaction that initiates the withdrawal.
The withdrawn tokens are not usable on Tezos until after the commitment with the withdrawal transaction is cemented, which takes two weeks. After the commitment is cemented, you can run the transaction to release the tokens on Tezos. See Withdrawal process.
The CLI reads its network parameters (RPC URLs, the Smart Rollup address, the withdrawal precompiles, the indexer URL) and a funded test account from a per-network config file under networks/, selected with the NETWORK environment variable:
NETWORK=shadownet-tezosx uv run bridge monitor_depositsIf NETWORK is unset it defaults to shadownet-etherlink. The available networks are the *.toml file stems under networks/ (e.g. shadownet-etherlink, shadownet-tezosx); add a new one by dropping a config file there.
Any option passed on the command line overrides the value from the config, so the example commands above spell out --tezos-rpc-url, --smart-rollup-address, the private keys, etc. to be self-contained; on a configured network you can omit those and let the config fill them in.
- Install Foundry by following the installation guide
Note
The version of forge used to build contracts is forge 0.2.0 (41d4e54 2024-09-17T00:18:07.762487140Z).
-
Install LIGO with the instructions at https://ligolang.org.
-
Install Solidity dependencies with Forge (part of the Foundry toolchain). Installation should be executed from the
etherlinkdirectory:
(cd etherlink && forge install)Forge uses git submodules to manage dependencies. It is possible to check versions of the Solidity libraries installed by running git submodule status. Here are the versions used to compile contracts:
1d9650e951204a0ddce9ff89c32f1997984cef4d etherlink/lib/forge-std (v1.6.1)
fd81a96f01cc42ef1c9a5399364968d0e07e9e90 etherlink/lib/openzeppelin-contracts (v4.9.3)To compile Tezos-side contracts, the LIGO compiler must be installed. The most convenient method is to use the Docker version of the LIGO compiler. Compilation of all contracts using the dockerized LIGO compiler can be initiated with the following command:
uv run bridge build_tezos_contractsNote
This repository includes built Tezos side contracts which are located in the tezos/build directory.
To compile contracts on the Etherlink side, Foundry must be installed. To initiate the compilation, navigate to the etherlink directory and run forge build, or execute the following script from the root directory:
uv run bridge build_etherlink_contractsNote
This repository includes built Etherlink side contracts which are located in the etherlink/build directory.
The testing stack for Tezos contracts is based on Python and requires uv, pytezos, and pytest to be installed.
To run tests for the Tezos contracts, execute:
uv run pytest tezos/testsThe Etherlink contract tests use the Foundry stack and are implemented in Solidity. To run these tests, navigate to the etherlink directory and run forge test, or execute the following script from the root directory:
uv run bridge etherlink_testsThe tests under scripts/tests are integration tests: they run a full deposit / withdrawal flow against a live Tezos + Etherlink network and a running bridge indexer, signing real on-chain operations with the funded account configured in scripts/tests/conftest.py.
Because they need live infrastructure (and cost testnet funds), they are gated behind the integration marker and deselected by default — the plain uv run pytest above runs only the offline contract tests.
These tests are stateful and ordered (see function_order in conftest.py): they run as a chain — indexer health check → asset whitelist checks → deposit → create withdrawal → finish withdrawal. Run them together, not individually; e.g. the finish step looks up a pending withdrawal that an earlier step created. The health check is marked critical: if it fails, the run aborts immediately.
The withdrawal completion step needs the rollup challenge (refutation) window to elapse before the outbox message can be executed. That window is long on shadownet (~2 weeks), so the finish test carries its own cementation marker and is excluded from the normal integration run.
# Offline contract tests only (default — no network needed):
uv run pytest
# Fast iteration — single token, no long wait, verbose:
uv run pytest -m "integration and not cementation" -k "USDt or healthy" -v
# Integration flow without the long wait (deposit + create withdrawal, all tokens):
uv run pytest -m "integration and not cementation"
# Full flow including withdrawal completion — only on a network with a SHORT
# challenge window, where cementation happens quickly:
uv run pytest -m integration
# On shadownet (~2-week window) the completion can't finish in one run.
# First run the integration flow above to CREATE the withdrawal, wait for the
# window to pass, then finalize it (it finds the now-cemented pending
# withdrawal in the indexer and executes the outbox message):
uv run pytest -m cementationuv run bridge bootstrap deploys a fresh token set (Token + Ticketer + ERC20 proxy + Bridge Helper) for each MAINNET_WHITELIST token. It's interactive — pick a config from networks/*.toml (or Custom). Copy the printed contract addresses into that config's [[tokens]].
monitor_deposits / monitor_withdrawals print bridge stats from the active network's indexer (NETWORK env), broken down by token, status, and — for withdrawals — kind (regular vs the fast-withdrawal variants). They sample the most recent --limit operations (newest first) and report whether older ones were left out.
NETWORK=shadownet-tezosx uv run bridge monitor_deposits --limit 500
NETWORK=shadownet-tezosx uv run bridge monitor_withdrawals --limit 500To perform linting, execute the following commands:
uv run mypy .
uv run ruff check .
uv run black .docker run --rm etherlink-bridge bridge_token \
--token-address KT19P1nbGzGnumMfRHcLNuyQUdcuwjpBfsCU \
--token-type FA1.2 \
--token-decimals 8 \
--token-symbol "TST" \
--token-name "Test Token" \
--tezos-private-key edsk4XG4QyAj19dr78NNGH6dpXBtTnkmMdAkM9w5tUTCHaUP1pJaD5 \
--tezos-rpc-url "https://rpc.tzkt.io/shadownet" \
--etherlink-private-key f463e320ed1bd1cd833e29efc383878f34abe6b596e5d163f51bb8581de6f8b8 \
--etherlink-rpc-url "https://node.shadownet.etherlink.com" \
--skip-confirmdocker run --rm etherlink-bridge fa_deposit \
--token-bridge-helper-address KT1Ejrkzge6GoqUc7rWRT6kQKD5FYueKyjvH \
--amount 77 \
--receiver-address 0x7e6f6CCFe485a087F0F819eaBfDBfb1a49b97677 \
--smart-rollup-address sr19fMYrr5C4qqvQqQrDSjtP31GcrWjodzvg \
--tezos-private-key edsk4XG4QyAj19dr78NNGH6dpXBtTnkmMdAkM9w5tUTCHaUP1pJaD5 \
--tezos-rpc-url "https://rpc.tzkt.io/shadownet"docker run --rm etherlink-bridge fa_withdraw \
--erc20-proxy-address 0x3dFF505A2A69e6e0b05fDB71b5F6DDd514fDaF47 \
--amount 17 \
--tezos-side-router-address KT1LdyznoJDzUqsgE8zpM242W1BBi42S8img \
--ticketer-address-bytes 0x01843d2272438cbe9bdc32423c41cb9cfc785e381c00 \
--ticket-content-bytes 0x0707000005090a000000b90502000000b307040100000010636f6e74726163745f616464726573730a000000244b543157633773586a70436b7a59454465727478526339736e776775744658384d6d717607040100000008646563696d616c730a0000000136070401000000046e616d650a0000000a546574686572205553440704010000000673796d626f6c0a000000045553447407040100000008746f6b656e5f69640a00000001300704010000000a746f6b656e5f747970650a00000003464132 \
--receiver-address tz1ekkzEN2LB1cpf7dCaonKt6x9KVd9YVydc \
--etherlink-private-key f463e320ed1bd1cd833e29efc383878f34abe6b596e5d163f51bb8581de6f8b8 \
--etherlink-rpc-url "https://node.shadownet.etherlink.com"