POA - Part 2 - Bridge assets between a sidechain and a mainchain

This article is part of a POA tutorial series:


The POA Bridge is a solution to transfer asset tokens (Native and ERC20/ERC677 tokens) between two Ethereum chains.

An asset token usually has two purposes:

Both usages require different network properties to enable the best experience, the monetary use may need a strong network security and liveness and an access to a large network of assets to facilitate trade while the application use needs faster and cheaper transactions for a better user experience.

As part of the layer 2 scalability solutions, sidechain and bridges implement this paradigm of two chains for two usages and try to solve the scalability and UX issues due to the Ethereum mainnet being usually considered too slow (15 tx/sec) and too expensive on gas fees to enable a good user experience for most of the use cases (games or social apps). In this context, the general flow is the following:

  1. User buys token on the mainchain
  2. User transfers his tokens to the sidechain via the bridge (double representation: locked on the mainchain and minted on the sidechain)
  3. User uses the tokens in a fast and efficient way. Perhaps earn or lose some tokens from other users
  4. User decides to exit his tokens from the sidechain and transfer them back to the mainchain via the bridge (tokens unlocked on the mainchain and burned on the sidechain)
  5. User sells his tokens on the mainchain

In this tutorial, we will learn how to deploy a token on the two networks (RinkeBy network as mainchain and POA Sokol as sidechain) and then deploy and use the bridge (ERC20 to ERC20) to let a user transfers his assets from one network to another.


In order to start, you will need the following programs installed on your machine:

$ npm install -g truffle

$ truffle version
Truffle v5.0.20 (core: 5.0.20)
Solidity v0.5.0 (solc-js)
Node v8.15.1
Web3.js v1.0.0-beta.37

Step 1: Deploy an ERC20 token called BRidge Token BRT on the mainchain (Rinkeby network)

  1. Let's first create a project folder for our ERC20 BRT and initialize a Truffle project.
$ mkdir token
$ cd token
$ truffle init

  1. Then we will install the Open-Zeppelin Solidity library which contains a lot of high-quality, fully tested and audited reusable smart contracts
$ npm init -y
$ npm install openzeppelin-solidity --save-exact

  1. Create a contract file ./contacts/BridgeToken.sol containing
// BridgeToken.sol
pragma solidity ^0.5.8;

import "openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol";

contract BridgeToken is ERC20Mintable {
    string public constant name = "Bridge Token";
    string public constant symbol = "BRT";
    uint8 public constant decimals = 18;

That's it and a big Thanks to the Zeppelin team for all the work they done for Ethereum. Basically our smart contract inherits from MintableToken which offers all the ERC-20 standard functionalities as well as functions to mint tokens. We only need to specify our token name "Bridge Token", its symbol "BRT" and the number of decimals (divisibility).

To make sure your smart contract compiles, you can execute the command truffle compile.

  1. Deploy the smart contract on the RinkeBy network

Note: Make sure the account used to deploy the contract is funded with RinkeBy ethers (see faucet).

Once our smart contracts compile, we need to deploy it. To do so, we need first to complete the migration script, create a file ./migrations/2_deploy_contract.js

// 2_deploy_contract.js
const BridgeToken = artifacts.require("./BridgeToken.sol");

module.exports = function(deployer, network, accounts) {
    // Deploy the smart contract
    deployer.deploy(BridgeToken, {from: accounts[0]}).then(function(instance) {
        // Mint 100 tokens
        return instance.mint(accounts[0], web3.utils.toBN("100000000000000000000"), {from: accounts[0]});

The migration script deploys the contract and additionally mint and distribute 100 BRT tokens to the deployer account.

Next step consists in configuring a connection to the RinkeBy network in order to deploy a smart contract.

Install the following dependencies (dotenv to manage environment variables and truffle-hdwallet-provider to sign transactions from an account derived from a mnemonic)

$ npm install --save dotenv truffle-hdwallet-provider

Create a file ./.env to store some private information, we do not want to share anywhere (gitignore this file)

// .env
MNEMONIC=twelve words you can find in metamask/settings/reveal seed words 

Finally let's configure the connection to the RinkeBy network. Edit the file ./truffle.js

// truffle.js
const HDWalletProvider = require("truffle-hdwallet-provider");

module.exports = {
  networks: {
    development: {
      host: "localhost",
      port: 8545,
      network_id: "*"
    rinkeby: {
        provider: new HDWalletProvider(process.env.MNEMONIC, "https://rinkeby.infura.io/v3/" + process.env.INFURA_API_KEY),
        network_id: 4,
        gas: 4500000

To deploy the smart contracts on the RinkeBy network, run the command (this might take a little while) :

$ truffle migrate --network rinkeby

Compiling your contracts...
> Compiling ./contracts/BridgeToken.sol
> Compiling ./contracts/Migrations.sol
> Compiling openzeppelin-solidity/contracts/access/Roles.sol
> Compiling openzeppelin-solidity/contracts/access/roles/MinterRole.sol
> Compiling openzeppelin-solidity/contracts/math/SafeMath.sol
> Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20.sol
> Compiling openzeppelin-solidity/contracts/token/ERC20/ERC20Mintable.sol
> Compiling openzeppelin-solidity/contracts/token/ERC20/IERC20.sol
> Artifacts written to /home/gjeanmart/workspace/tutorials/bridge_token/build/contracts
> Compiled successfully using:
   - solc: 0.5.8+commit.23d335f2.Emscripten.clang

Migrations dry-run (simulation)
> Network name:    'rinkeby-fork'
> Network id:      4
> Block gas limit: 0x7244c0


   Deploying 'Migrations'
   > block number:        4502550
   > block timestamp:     1559667907
   > account:             0xF0f15Cedc719B5A55470877B0710d5c7816916b1
   > balance:             33.578282390129999997
   > gas used:            246393
   > gas price:           2 gwei
   > value sent:          0 ETH
   > total cost:          0.000492786 ETH

   > Total cost:         0.000492786 ETH


   Deploying 'BridgeToken'
   > block number:        4502552
   > block timestamp:     1559667919
   > account:             0xe9B0E206C8cA079bca49F0120abfD02760093612
   > balance:             99.996785462
   > gas used:            1607269
   > gas price:           2 gwei
   > value sent:          0 ETH
   > total cost:          0.003214538 ETH

   > Total cost:         0.003214538 ETH

> Total deployments:   2
> Final cost:          0.003707324 ETH

Starting migrations...
> Network name:    'rinkeby'
> Network id:      4
> Block gas limit: 0x724802


   Deploying 'Migrations'
   > transaction hash:    0x44dbbf18d316adb29143d1b3341c1e28b297d144411ee98cb23017270f77b9ed
   > Blocks: 1            Seconds: 9
   > contract address:    0xAC96dc3AC9baB86c7d89a5868096394CB708a6a0
   > block number:        4502551
   > block timestamp:     1559667943
   > account:             0xF0f15Cedc719B5A55470877B0710d5c7816916b1
   > balance:             33.573547316129999997
   > gas used:            261393
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.00522786 ETH

   > Saving migration to chain.
   > Saving artifacts
   > Total cost:          0.00522786 ETH


   Deploying 'BridgeToken'
   > transaction hash:    0x80dc122178131cbd040e90b667cc1d11a47d21abf8ebf17c80232b1c4c5f33df
   > Blocks: 2            Seconds: 21
   > contract address:    0x40A6a864133985E1146DDfEb48c7391CD07596F5
   > block number:        4502554
   > block timestamp:     1559667988
   > account:             0xF0f15Cedc719B5A55470877B0710d5c7816916b1
   > balance:             33.540261476129999997
   > gas used:            1622269
   > gas price:           20 gwei
   > value sent:          0 ETH
   > total cost:          0.03244538 ETH

   > Saving migration to chain.
   > Saving artifacts
   > Total cost:          0.03244538 ETH

> Total deployments:   2
> Final cost:          0.03767324 ETH

As a result, we can identify our Smart Contract BridgeToken as been deployed at the address 0x40A6a864133985E1146DDfEb48c7391CD07596F5 (see block explorer)

Step 2: Initialise the monorepo tokenbridge

In this second step, we will initialise the GitHub mono-repository in order to install each component in the following steps.

  1. Clone the repo
$ cd ../
$ git clone --recursive https://github.com/poanetwork/tokenbridge
$ cd ./tokenbridge/
  1. Install the depandancies
$ yarn install && yarn install:deploy

Step 3: Configure and Deploy the bridge contracts

In this third step, we will deploy the necessary contracts to enable a ERC20 to ERC20 bridge.

  1. Go to the contracts folder
$ cd ./contracts/

2. Compile the smart contracts

$ npm run compile

3. Create a configuration file in ./deploy/.env

Note 1 : the following properties to change

Note 2 : For the reason of the tutorial, we decided to simplify the configuration as much as possible (one account administrating and validating)

Note 3 : Make sure the account ACCOUNT_ADMIN is funded with RinkeBy ethers and POA Sokol ethers.

Note 4 : No block reward (rewardable token) has been configured.


## If Home network does not support byzantium fork, should use contracts compiled for spuriousDragon

## If Foreign network does not support byzantium fork, should use contracts compiled for spuriousDragon






##for bridge erc_to_erc and erc_to_native mode
## Only for for erc_to_erc mode

##If several validators are used, list them separated by space without quotes
##E.g. VALIDATORS=0x 0x 0x
##Set to ONE_DIRECTION or BOTH_DIRECTIONS if fee will be charged on home side, set to false otherwise
## Valid only for rewards on erc_to_native mode. Supported values are BRIDGE_VALIDATORS_REWARD and POSDAO_REWARD
##Set to ONE_DIRECTION or BOTH_DIRECTIONS if fee will be charged on foreign side, set to false otherwise
##If HOME_REWARDABLE or FOREIGN_REWARDABLE set to true, list validators accounts were rewards should be transferred separated by space without quotes

## Fee to be taken for every transaction directed from the Home network to the Foreign network
## E.g. 0.1% fee
## Fee to be taken for every transaction directed from the Foreign network to the Home network
##for bridge native_to_erc, erc_to_erc mode

4. Deploy the Bridge configuration

$ ./deploy.sh
Deployment has been completed.

[   Home  ] HomeBridge: 0xc4e7cA947521f331969e41CC7c99ADa22F2C7F9C at block 9044640
[   Home  ] ERC677 Bridgeable Token: 0xEa3acD04DdaF1F1A5Ae1B9f5f690123aA4E19B36
[ Foreign ] ForeignBridge: 0xeb2dbC5AB9380A3517AcA9d8CA0c39873e569a93 at block 4503560
[ Foreign ] ERC20 Token: 0x40A6a864133985E1146DDfEb48c7391CD07596F5
Contracts Deployment have been saved to `bridgeDeploymentResults.json`
    "homeBridge": {
        "address": "0xc4e7cA947521f331969e41CC7c99ADa22F2C7F9C",
        "deployedBlockNumber": 9044640,
        "erc677": {
            "address": "0xEa3acD04DdaF1F1A5Ae1B9f5f690123aA4E19B36"
    "foreignBridge": {
        "address": "0xeb2dbC5AB9380A3517AcA9d8CA0c39873e569a93",
        "deployedBlockNumber": 4503560

Save the JSON information above.

Step 4: Configure and deploy the Bridge Oracle

  1. Go to the `oracle folder
$ cd ../oracle

2. Create a configuration file in ./.env

Note 1 : Open the saved JSON file bridgeDeploymentResults.json to get the home and foreign bridge contract address and deployment block numbers.

    "homeBridge": {
        "address": "0xc4e7cA947521f331969e41CC7c99ADa22F2C7F9C",
        "deployedBlockNumber": 9044640,
        "erc677": {
            "address": "0xEa3acD04DdaF1F1A5Ae1B9f5f690123aA4E19B36"
    "foreignBridge": {
        "address": "0xeb2dbC5AB9380A3517AcA9d8CA0c39873e569a93",
        "deployedBlockNumber": 4503560

Note 2 : the following properties to change

Note 3 : For the reason of the tutorial, we decided to simplify the configuration as much as possible (one account administrating and validating)

Note 4 : Make sure the account ACCOUNT_ADMIN is funded with RinkeBy ethers and POA sokol ethers.

HOME_BRIDGE_ADDRESS=bridgeDeploymentResults.json / homeBridge / address
FOREIGN_BRIDGE_ADDRESS=bridgeDeploymentResults.json / foreignBridge / address





HOME_START_BLOCK=bridgeDeploymentResults.json / homeBridge / deployedBlockNumber
FOREIGN_START_BLOCK=bridgeDeploymentResults.json / foreignBridge / deployedBlockNumber


3. Build and run the Bridge Oracle (using Docker and Docker-compose)

This docker package is composed of a Reddit database, Rabbit MQ broker and NodeJS workers.

$ docker-compose up --build

Use the flag -d to run the Bridge Oracle in the background (daemon)

Step 5: Configure and Deploy the bridge UI

Last step consist in deploying a User Interface to transfer tokens between the sidechain and the mainchain.

  1. Navigate to the folder ui
$ cd ../ui

2. Create a configuration file in ./.env

Note 1: Open the saved JSON file bridgeDeploymentResults.json to get the home and foreign bridge contract address and deployment block numbers.

    "homeBridge": {
        "address": "0xc4e7cA947521f331969e41CC7c99ADa22F2C7F9C",
        "deployedBlockNumber": 9044640,
        "erc677": {
            "address": "0xEa3acD04DdaF1F1A5Ae1B9f5f690123aA4E19B36"
    "foreignBridge": {
        "address": "0xeb2dbC5AB9380A3517AcA9d8CA0c39873e569a93",
        "deployedBlockNumber": 4503560
REACT_APP_HOME_BRIDGE_ADDRESS=bridgeDeploymentResults.json / homeBridge / address
REACT_APP_FOREIGN_BRIDGE_ADDRESS=bridgeDeploymentResults.json / foreignBridge / address


3. Run Bridge UI

$ npm start

4. Open your Internet Browser, unlock Metamask on the Rinkeby network with the account used to deploy BRT token and go to http://localhost:3000/

If you are on the RinkeBy network, you should see that you own 100 BRT token on the mainchain (RinkeBy) and 0 on the sidechain (POA Sokol)

You can now transfer BRT token between the mainchain and the sidechain: