Blockchain technology offers providential results. Its potential for improving business processes, providing transactional transparency and security in the value chain, and reducing operational costs is obvious.
The past few years have seen continuous growth in blockchain-related projects. It signifies that developers are leveraging blockchain’s capabilities by thinking outside the box. Besides, we have to understand there is no perfect solution to address all blockchain needs at once.
Each day the number of solutions that rely on blockchain technology is increasing. But, the technology’s evolution is taking a hit due to the lack of interoperability among blockchain solutions. Many solutions are available for blockchain interoperability, all having their pros and cons. I have used one such solution, Chainbridge, which is an extensible cross-chain communication protocol. It currently supports bridging between EVM and Substrate based chains.
In this blog, we’ll discuss a specific use case of supply chain management that uses the blockchain interoperability between substrate and ethereum chains.
The Usecase
Blockchain holds great promise in the area of supply chain management. It can improve supply chains by enabling faster and more cost-efficient product delivery, enhancing product traceability, improving coordination between partners, and aiding access to financing.
The usecase I have is a simplified version of champagne bottle supply-chain management. I did so to learn and showcase substrate development and the interoperability between Substrate and Ethereum blockchains. By using this application, the end consumer can track and verify the authenticity of the champagne bottle.
Every bottle created will have a unique id, which we will use to track it further down the process. The use-case source code can be found here. Now let’s look at the list of actors and their respective roles in the system.
Actors and Roles
For simplicity, I will go with these four types of actors. Their roles are as follows.
Manufacturer
- Bottle Creation – Creates and registers new bottles in the system.
- Shipment Registration – Create a new shipment, assign a carrier to complete the delivery, and provide the retailer details and bottles to be delivered.
Carrier
- Pickup Shipment – Picks up the shipment from the manufacturer.
- Deliver Shipment – Delivers the shipment to the retailer.
Retailer
- Sell Bottle – Sells the bottles to the end customer.
Customer
- Buy Bottles – Buys bottles from the retailer.
System Modules or Substrate Pallets
The entire supply chain process of the application is built with two modules.
Registrar: Registrar pallet is responsible for registering and keeping a record of various actors and bottles in the system. It exposes some functions like registerManufacturer(), registerCarrier() etc to register the members of a particular type. A manufacturer can invoke the registerBottle() function to register a new bottle with a unique id in the system.
Bottle Tracking: The bottle shipment process is tracked using this module. Functions registerShipment() and trackShipment() are used for tracking the bottle from shipment registration to delivery to the retailer. For final customer sell, sellToCustomer() function is called, which transfers the bottle ownership to the end customer.
Chainbridge pallets –
Chainbridge, example-pallet, and example-erc721: Chainbridge provides these three pallets for interchain communication. Follow Chainbridge-substrate for their documentation.
Process Flow
Let’s discuss the entire process from bottle creation to end customer sales, step by step. We’ll have a look at the external function used to interact with the application for each step.
Member Registration
First of all, we have to register various actors in the system. There are four types of actors. So, we have to use four functions, registering each of them using the registrar pallet. These are registerManufacturer(), registerCarrier(), registerRetailer() and registerCustomer(). All 4 functions have the same function signature, one is explained below.
Function Signature:
registerManufacturer()
It takes no argument. The caller of the function is registered as a manufacturer. There can be any number of manufacturer, carrier, etc. If there are multiple manufacturers, all can invoke this function separately to register themselves. The same goes for carriers, retailers, and customers.
Bottle Creation
A manufacturer can register a new bottle.
Function Signature:
registerBottle(id: BottleId)
The manufacture will invoke this method from the registrar pallet providing the BottleId to be registered.
Shipment Registration
The shipment will be registered by the manufacturer.
Function Signature:
registerShipment(id: ShipmentId, carrier: AccountId, retailer: AccountId, bottles: Vec<BottleId>)
To register a shipment, the manufacturer will provide a unique ShipmentId, account Id of the carrier who will carry the package, account id of the retailer to which shipment has to be delivered and the list of bottle ids to be delivered.
Shipment Pickup and Delivery
The assigned carrier will pick up the shipment from the manufacturer and deliver it to the retailer.
Function Signature:
trackShipment(id: ShipmentId, operation: ShipmentOperation)
The carrier will provide the Shipment Id and the operation that it wants to perform on the shipment for tracking a shipment. The ShipmentOperation is an enum that holds two values: Pickup and Deliver. After the delivery operation is completed, the bottle will be in the ownership of the retailer.
End Customer Sell and Payments
In all the steps performed before, it is assumed that all the payments have been made off-chain. To sell the bottle to the end customer, I have assumed that the process will be initiated off-chain where both the parties (Customer and Retailer) will agree on how many bottles have to be sold and the total amount. Once the customer transfers the agreed amount to the retailer’s substrate account, the off-chain system will automatically trigger/invoke a method in the substrate chain, i.e., sellToCustomer(), which will only transfer the ownership of the bottles to the customer.
Function Signature:
sellToCustomer(customer: AccountId, bottles: Vec<BottleId>)
This function will be invoked using the retailer’s account, providing the customer’s account id and the bottles to be sold.
The customer can transfer the amount to the retailer’s substrate account by any means. But to support our interchain operability use case perspective, let us assume that the customer has some Ethereum smart contract tokens. This token holds some equivalent value to the substrate native token. Now the customer wants to transfer these Ethereum tokens directly to the retailer’s substrate account. This interchain communication can be achieved using Chainbridge, a cross-chain communication protocol.
Chainbridge
Chainbridge is a modular multi-directional blockchain bridge. Currently, it supports interoperability between Ethereum and Substrate-based chains. There are three main roles:
-
- Listener: it extracts events from a source chain and constructs the message
- Router: its role is to pass the message from Listener to Writer
- Writer: interprets messages and sends transactions to the target chain.
Both sides of the bridge have a set of smart contracts (or pallets in the substrate), where each has a specific function:
-
- Bridge – Interaction between users and relayers happens using the bridge. It starts a transaction on the source chain, executes proposals on the target chain and delegates calls to the handler contracts for deposits.
- Handler- validates the parameters provided by the user, creating a deposit/execution record.
- Target – as the name suggests, this is the contract we are going to interact with on each side of the bridge.
Below diagram is a summarized workflow of Chainbridge:
Chainbridge currently relies on trusted relayers. However, it has mechanisms to stop power abuse and mishandling of funds by any single relayer.
Chainbridge Setup
Prerequisites
- Docker
- ChainBridge v1.1.1 binary (ChainSafe/ChainBridge)
- Polkadot JS Portal (Polkadot/Substrate Portal)
- Provenance Usecase (https://github.com/TalenticaSoftware/provenance-usecase/tree/master/substrate)
- cb-sol-cli (cb-sol-cli)
Starting Local Chains
Follow the instructions at provenance usecase repo to start the substrate chain.
The command below will start the geth instance.
docker run -p 8545:8545 chainsafe/chainbridge-geth:20200505131100-5586a65 |
Ethereum Chain Setup
Deploy Contracts
To deploy the contracts onto the Ethereum chain, run the following:
cb-sol-cli deploy –all –relayerThreshold 1 |
Register fungible resource
cb-sol-cli bridge register-resource –resourceId “0x000000000000000000000000000000c76ebe4a02bbc34786d860b355f5a5ce00” –targetContract “0x21605f71845f372A9ed84253d2D024B7B10999f4” |
Specify Token Semantics
# Register the erc20 contract as mintable/burnable cb-sol-cli bridge set-burn –tokenContract “0x21605f71845f372A9ed84253d2D024B7B10999f4” # Register the associated handler as a minter cb-sol-cli erc20 add-minter –minter “0x3167776db165D8eA0f51790CA2bbf44Db5105ADF” |
Substrate Chain Setup
Register Relayer
Select the Sudo tab in the PolkadotJS UI. Choose the addRelayer method of chainBridge, and select Alice as the relayer.
Register Resources For Fungible Transfer
Select the Sudo tab and call chainBridge.setResource with the below method parameters:
Id: 0x000000000000000000000000000000c76ebe4a02bbc34786d860b355f5a5ce00
Method: 0x4578616d706c652e7472616e73666572 (utf-8 encoding of “Example.transfer”) |
Whitelist Chains
Using the Sudo tab, call chainBridge.whitelistChain, specifying 0 for ethereum chain ID.
Running A Relayer
Here is an example configuration for a single relayer (“Alice”) using the contracts we’ve deployed. Save this JSON inside a file and name it config.json.
{ “chains”: [ { “name”: “eth”, “type”: “ethereum”, “id”: “0”, “endpoint”: “ws://localhost:8545”, “from”: “0xff93B45308FD417dF303D6515aB04D9e89a750Ca”, “opts”: { “bridge”: “0x62877dDCd49aD22f5eDfc6ac108e9a4b5D2bD88B”, “erc20Handler”: “0x3167776db165D8eA0f51790CA2bbf44Db5105ADF”, “erc721Handler”: “0x3f709398808af36ADBA86ACC617FeB7F5B7B193E”, “genericHandler”: “0x2B6Ab4b880A45a07d83Cf4d664Df4Ab85705Bc07”, “gasLimit”: “1000000”, “maxGasPrice”: “20000000” } }, { “name”: “sub”, “type”: “substrate”, “id”: “1”, “endpoint”: “ws://localhost:9944”, “from”: “5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY”, “opts”: { “useExtendedCall” : “true” } } ] } |
First, pull the Chainbridge docker image.
docker pull chainsafe/chainbridge:latest |
Then start the relayer as a docker container.
docker run -v $(pwd)/config.json:/config.json –network host chainsafe/chainbridge –testkey alice –latest |
With this setup complete, now we should be able to do fungible transfers over the two chains.
Interchain token transfer
Substrate Native Token ⇒ Ethereum ERC 20
In the Polkadot JS UI select the Developer -> Extrinsics tab and call example.transferNative with these parameters:
-
- Amount: 1000 Unit
- Recipient: 0xff93B45308FD417dF303D6515aB04D9e89a750Ca
- Dest Id: 0
To query the recipients balance on ethereum use this:
cb-sol-cli erc20 balance –address “0xff93B45308FD417dF303D6515aB04D9e89a750Ca” |
Ethereum ERC20 ⇒ Substrate Native Token
If necessary, tokens can be minted:
cb-sol-cli erc20 mint –amount 1000 |
Before initiating the transfer we have to approve the bridge to take ownership of the tokens:
cb-sol-cli erc20 approve –amount 1000 –recipient “0x3167776db165D8eA0f51790CA2bbf44Db5105ADF” |
To initiate a transfer on the ethereum chain use this command (Note: there will be a 10 block delay before the relayer will process the transfer):
cb-sol-cli erc20 deposit –amount 1 –dest 1 –recipient “0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d” –resourceId “0x000000000000000000000000000000c76ebe4a02bbc34786d860b355f5a5ce00” |
Chainbridge Findings
-
- At the time of writing this blog, the version of Chainbridge-substrate pallet is not supported with the substrate-parachain-template. This makes it hard to use the bridge with Polkadot parachains.
-
- On the substrate chain, the provided Chainbridge-substrate handlers do not mint/burn substrate native tokens. Which leads to inconsistency. Example case:
-
-
- When sending tokens from Substrate -> Ethereum. On the substrate chain, tokens are transferred to the Chainbridge account and on the Ethereum chain, tokens are minted/released to the receivers account.
- After this transaction completes, on the substrate chain, whatever amount is received by the bridge account is then will be available to transfer at the time of Ethereum -> Substrate transaction.
- This implies that if our first transaction is to transfer tokens from Ethereum -> Substrate, we will not receive anything on the substrate chain. Because on the substrate chain, the bridge account does not have any tokens to transfer to. In such cases, the transaction is not reverted and inconsistent balances are stored on both sides.
-
-
- It is a fully trusted system; admin users are in control of reviewed components. For instance, minting an infinite amount of tokens or withdrawing all the tokens from the Bridge contract on the original chain is a risk.
- Users should have full trust in admin and relayers-based consensus not only when using the Bridge, but also when using tokens created by the Bridge (tokens on a foreign blockchain that represent the original tokens).
-
- For efficient operation of the whole system, you need to depend on a significant amount of configuration and additional manual work.
Conclusion
Blockchain-based networks are being built to offer specific capabilities. These different networks should be able to share their data and talk to each other to make the most out of these capabilities, which makes blockchain interoperability a must.
Chainbridge offers an excellent solution with its modular and multi-directional design. They have been continuously working towards enhancing the bridge system and their community channels are very active and supportive. With version 1.0, Chainbridge has built the scaffoldings necessary for message passing functionality. The upcoming versions of Chainbridge can achieve this functionality in a decentralized and trustless manner to ensure there are no central points of failure.