← All news

Bridging Tezos and Etherlink

Follow the journey of a token from Tezos to Etherlink and back

Bridging Tezos and Etherlink

Get ready to dive into something truly exciting — a world where Tezos and Ethereum work together!

If you’ve ever wondered what it’s like to have the best of both worlds, Etherlink on Tezos is your answer. Imagine being able to play in Ethereum’s vast playground while still enjoying the unique perks and security of Tezos. Sounds like a dream, right? But it’s real and it’s here, thanks to the Etherlink Bridge SDK.

Whether you’re a coding wizard or just blockchain-curious, this guide is meant for you. We’re going to explain, step-by-step, how you can send tokens on a little round-trip from Tezos to Etherlink and back, without the usual blockchain hassle.

So, grab your favorite beverage, settle in, and let’s make some magic happen in the blockchain world. Ready to bridge the gap and expand your dapp’s universe? Let’s get started!

Note: this article is meant for developers with an intermediate level in TypeScript and a basic level in CameLigo.

The Bridge SDK

We are now going to follow the journey of 0.01 tzBTC sent from the Tezos L1 to Etherlink and back. At the beginning of this journey, there is a transaction created by a user. This is where you will use the Bridge SDK for the first time.

The Baking Bad team has done an amazing job putting together a TypeScript SDK to abstract all the complex steps you would otherwise need to follow to create transactions to transfer tokens between Tezos and Etherlink.

The SDK is a regular NPM package that you can download with the following command in the terminal:

npm install @baking-bad/tezos-etherlink-bridge-sdk

To initiate the transaction on Tezos, you will also need the Taquito package with the beacon-wallet package to handle building, signing and sending the transaction:

npm install @taquito/taquito @taquito/beacon-wallet

To interact with Etherlink, you will need to use web3 or a similar library to interact with the Ethereum ecosystem:

npm install web3

Setting up the bridge

Before using the SDK, you must set up Taquito because the configuration of the bridge requires an instance of the TezosToolkit provided by Taquito:

import { BeaconWallet} from '@taquito/beacon-wallet';
import { BeaconEvent } from '@airgap-it/beacon-sdk';
import { TezosToolkit, NetworkType } from '@taquito/taquito';
const tezosRpcUrl = 'https://ghostnet.ecadinfra.com';
const tezosToolkit = new TezosToolkit(tezosRpcUrl);
const beaconWallet = new BeaconWallet({
  name: 'My Etherlink Bridge',
  network: { type: NetworkType.GHOSTNET, rpcUrl: tezosRpcUrl }
});
await beaconWallet.client.subscribeToEvent(
  BeaconEvent.ACTIVE_ACCOUNT_SET,
  async (account) => {
    // Do something when the active account changes
  },
);
await beaconWallet.requestPermissions();
tezosToolkit.setWalletProvider(beaconWallet);

After that, it is also necessary to create an instance of web3.js to be used by the bridge:

import Web3 from 'web3';
// Using MetaMask
const web3 = new Web3("https://node.mainnet.etherlink.com");

Now, you can create an instance of the TokenBridge. The first thing to do is to configure the token pairs on Tezos and Etherlink:

import type { TokenPair } from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
const tokenPairs: TokenPair[] = [  // Native
  {
    tezos: {
      type: 'native',
      ticketHelperContractAddress: 'KT1VEjeQfDBSfpDH5WeBM5LukHPGM2htYEh3',
    },
    etherlink: {
      type: 'native',
    }
  },
  // tzBTC
  {
    tezos: {
      type: 'fa1.2',
      address: 'KT1HmyazXfKDbo8XjwtWPXcoyHcmNPDCvZyb',
      ticketerContractAddress: 'KT1H7if3gSZE1pZSK48W3NzGpKmbWyBxWDHe',
      ticketHelperContractAddress: 'KT1KUAaaRMeMS5TJJyGTQJANcpSR4egvHBUk',
    },
    etherlink: {
      type: 'erc20',
      address: '0x8e73aE3CF688Fbd8368c99520d26F9eF1B4d3BCa',
    }
  },
  // USDt
  {
    tezos: {
      type: 'fa2',
      address: 'KT1V2ak1MfNd3w4oyKD64ehYU7K4CrpUcDGR',
      tokenId: '0',
      ticketerContractAddress: 'KT1S6Nf9MnafAgSUWLKcsySPNFLUxxqSkQCw',
      ticketHelperContractAddress: 'KT1JLZe4qTa76y6Us2aDoRNUgZyssSDUr6F5',
    },
    etherlink: {
      type: 'erc20',
      address: '0xf68997eCC03751cb99B5B36712B213f11342452b',
    }
  }
];

The token pairs describe the type of token, the addresses of their respective contracts on Tezos and Etherlink, and the addresses of the ticketer contracts that will participate in the bridging.

The second thing to do is to create a defaultDataProvider that will use the token pairs you defined:

import {
  TokenBridge, DefaultDataProvider,
  defaultEtherlinkKernelAddress, defaultEtherlinkWithdrawPrecompileAddress
} from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
const defaultDataProvider = new DefaultDataProvider({
  tokenPairs,
  dipDup: {
    baseUrl: 'https://testnet.bridge.indexer.etherlink.com',
    webSocketApiBaseUrl: 'wss://testnet.bridge.indexer.etherlink.com'
  },
  tzKTApiBaseUrl: 'https://api.ghostnet.tzkt.io',
  etherlinkRpcUrl: 'https://node.ghostnet.etherlink.com',
})

The defaultDataProvider registers different URLs that will be used by the bridge to interact with Tezos and Etherlink, as well as the address of the indexer you want to use.

After the provider has been created, it is time to create the instance of the token bridge:

import {
  DefaultDataProvider,
  TokenBridge,
  TaquitoWalletTezosBridgeBlockchainService,
  Web3EtherlinkBridgeBlockchainService,
} from '@baking-bad/tezos-etherlink-bridge-sdk';
// ...
const tokenBridge = new TokenBridge({
  tezosBridgeBlockchainService: 
    new TaquitoWalletTezosBridgeBlockchainService({
      tezosToolkit,
      smartRollupAddress: 'sr18wx6ezkeRjt1SZSeZ2UQzQN3Uc3YLMLqg'
    }),
  etherlinkBridgeBlockchainService: 
    new Web3EtherlinkBridgeBlockchainService({
      web3
    }),
  bridgeDataProviders: {
    transfers: defaultDataProvider,
    balances: defaultDataProvider,
    tokens: defaultDataProvider,
  }
});

On the Tezos side, it registers the instance of the TezosToolkit you created earlier to allow interactions with the blockchain and the address of the rollup running Etherlink.

On the Etherlink side, it registers the instance of web3.js used to interact with the EVM, the address of the Etherlink kernel and the address of the rollup outbox.

To finish, the defaultDataProvider must also be passed to get data regarding the tokens, the balances and the transfers on both sides.

Transferring tokens

After setting up the Bridge SDK, you can initiate the transfer of tokens. In this example, you will send tzBTC, an FA1.2 token available on Tezos and Etherlink.

First, import a couple of things you will need for the transfer of tokens:

import { BridgeTokenTransferStatus, type FA12TezosToken } from '@baking-bad/tezos-etherlink-bridge-sdk';
  • BridgeTokenTransferStatus allows you to track the status of the transfer to the rollup
  • FA12TezosToken is the type representing the token that will be sent (tzBTC)

After that, you add the instance of the TokenBridge that you created in the previous step.

Once the instance of the TokenBridge is ready, you will start the deposit:

const tzbtcTezosToken: FA12TezosToken = {
  type: 'fa1.2',
  address: 'KT1PWx2mnDueood7fEmfbBDKx1D9BAnnXitn',
};
// Deposit 0.01 tzBTC (8 decimals) to Etherlink (L2)
const { tokenTransfer, operationResult } = 
  await tokenBridge.deposit(100_000_000n, tzbtcTezosToken);

You must create an object of the type FA12TezosToken with a type property (either "fa.12" or "fa2") and an address property (the address of the token contract).

This object is required when calling the deposit method on the instance of the TokenBridge, along with the amount of tokens to be deposited in the Etherlink inbox.

After the transaction is sent, you can wait for an update on its status:

const finishedBridgeTokenDeposit = await tokenBridge.waitForStatus(
  tokenTransfer,
  BridgeTokenTransferStatus.Finished
);

The waitForStatus method takes the tokenTransfer returned by the above deposit function and the status you want to wait for, in this case, when the transfer is done, but it’s also possible to track its status when the transfer is created, pending or has failed.

If you want to specify a specific receiver for the transfer of tokens, you can pass their address as the third argument of the deposit method:

const etherlinkReceiverAddress = '0x...';
const { tokenTransfer, depositOperation } = 
  await tokenBridge.deposit(1_000_000n, tzbtcTezosToken, etherlinkReceiverAddress);

The deposit entrypoint of the contract

The deposit contract is a simple contract whose purpose is to take ownership of the tokens that need to be sent to Etherlink and issue a ticket that represents the tokens.

Get Claude Barde’s stories in your inbox

Join Medium for free to get updates from this writer.

Subscribe

Subscribe

Here is the code of the deposit entrypoint:

[@entry] 
let deposit
    (amount : TicketerDepositEntry.t)
    (store : Storage.t) : return_t =
let () = Assertions.no_xtz_deposit () in
let store = Storage.increase_total_supply amount store in
let self = Tezos.get_self_address () in
let sender = Tezos.get_sender () in
let ticket = Ticket.create store.content amount in
let token_transfer_op = Token.send_transfer store.token amount sender self in
let ticket_transfer_op = Ticket.send ticket sender in
[token_transfer_op; ticket_transfer_op], store
  1. The entrypoint receives the amount of tokens to be wrapped in the ticket.
  2. It verifies that no XTZ has been sent with the transaction.
  3. The total supply of tokens owned by the contract is increased.
  4. The contract fetches its own address, and the sender’s address and prepares an operation to take ownership of the user’s tokens.
  5. It creates the operation that will send the ticket to the Etherlink inbox.
  6. The entrypoint call returns the two operations that were created above and the new storage.

After these steps, the contract takes over the tokens that the user requested to be sent to Etherlink and a ticket is used and sent to Etherlink.

Now, let’s get your tokens back from Etherlink!

The Etherlink kernel offers a method called withdraw that must be called to withdraw tokens from the EVM rollup to the Tezos L1.

Because of the security measures implemented at the rollup level, the withdrawal of tokens requires a 15-day waiting period to give anyone enough time to refute malicious transactions. This is why this action happens in two steps.

The withdrawal request

Using the Etherlink Bridge SDK, you can send a withdrawal request to the rollup to transfer FA1.2 or FA2 tokens back to Tezos:

import { BridgeTokenTransferStatus, type ERC20EtherlinkToken } from '@baking-bad/tezos-etherlink-bridge-sdk';
/*
  The setup here is the same as the one described above
*/
const tzbtcEtherlinkToken: ERC20EtherlinkToken = {
  type: 'erc20',
  address: '0x8e73aE3CF688Fbd8368c99520d26F9eF1B4d3BCa'
};
// Withdraws 1 tzBTC (8 decimals) from Etherlink (L2)
const { operationResult, tokenTransfer } = 
  await tokenBridge.startWithdraw(1_000_000n, tzbtcEtherlinkToken);

The instance of TokenBridge offers a startWithdraw method that you can call to start the withdrawal. It takes two arguments, the amount of tokens you want to withdraw and an object representing the token to be withdrawn.

It returns an object with two properties:

  • operationResult is the operation to transfer the tokens on Etherlink to the withdraw method of the kernel
  • tokenTransfer is the operation to bridge the tokens from Etherlink to Tezos

The operationResult has a property called tokenTransfer that has itself a property called status that allows you to follow the status of the withdrawal operation.

When the status changes from pending to sealed, it means that the withdrawal is successful and you can jump to the second part of the withdrawal to get the tokens back in your wallet!

Finalizing the withdrawal

The TokenBridge instance offers a method called waitForStatus. This method allows you to wait until a specified status for the withdrawal transaction you created:

import { BridgeTokenTransferStatus } from '@baking-bad/tezos-etherlink-bridge-sdk';
/*
  The steps shown above
*/
const sealedBridgeTokenWithdrawal = await tokenBridge.waitForStatus(
  tokenTransfer,
  BridgeTokenTransferStatus.Sealed
);

Here, you are waiting for the transfer of tokens to have a sealed status. Once it is sealed, it will change to finished and the tokens will be unlocked on Tezos L1:

// uses the `sealedBridgeTokenWithdrawal` variable from above
const finishWithdrawResult = 
  await tokenBridge.finishWithdraw(sealedBridgeTokenWithdrawal);
const finishedBridgeTokenWithdrawal = 
  await tokenBridge.waitForStatus(
    finishWithdrawResult.tokenTransfer,
    BridgeTokenTransferStatus.Finished
  );

“Finished” means that the transaction in the outbox has been executed and confirmed, and the tokens have been returned to their owner.

The withdraw entrypoint of the contract

The transaction created and posted in the Etherlink outbox is a call to the contract that took ownership of the tokens and created a ticket representing the tokens to be sent to Etherlink.

The transaction goes to the withdraw entrypoint of the contract:

[@entry] 
let withdraw
    (params : RouterWithdrawEntry.t)
    (store : Storage.t)
    : return_t =
let { ticket; receiver } = params in
let (ticketer, (content, amount)), _ = Tezos.read_ticket ticket in
let () = assert_content_is_expected content store.content in
let () = Assertions.address_is_self ticketer in
let () = Assertions.no_xtz_deposit () in
let store = Storage.decrease_total_supply amount store in
let transfer_op = 
  Token.send_transfer store.token amount ticketer receiver in
  [transfer_op], store

Multiple things happen when the entrypoint is called:

  1. The entrypoint reads the parameters of the transaction from the outbox, i.e. the ticket and the address of the receiver of the tokens.
  2. It gets the details it will need later from the ticket that was created by the outbox and verifies that they are correct.
  3. It also verifies that no XTZ was sent to the contract.
  4. The total supply of tokens held by the contract is decreased by the amount that will be returned.
  5. The entrypoint forges a new transaction to return the tokens to the provided receiver and updates the contract storage.

Conclusion

In this comprehensive guide, you have explored the intricacies of token bridging between Tezos and Etherlink, leveraging the power of the Bridge SDK provided by Baking Bad.

Developers now have a great SDK to integrate seamless asset transfers into their applications, from setting up the necessary tools to understanding the underlying smart contract mechanics.

The TypeScript SDK provides the developers with an abstraction over the complexities of the token transfers between Tezos and Etherlink, and they can focus instead on creating user-centric applications that take advantage of both Ethereum’s development ecosystem and Tezos’ secure, scalable infrastructure.

Whether you’re transferring FA1.2 tokens like tzBTC or FA2 tokens like tzPEPE, the use of the Etherlink Bridge SDK is pivotal in ensuring these operations are both secure and user-friendly.

Understanding and utilizing this bridge will be crucial for developers looking to expand their applications across Tezos L1 and the Etherlink rollup.

Packages used in this article: