Skip to main content

A multi-chain storage app

note

Clone the examples repo and look at the app before starting!

This app exposes a number stored across two different chains. The StorageSender contract exposes a simple counter interface (store, retrieve, increment, and decrement the stored value); the StorageReceiver is simpler (it only exposes store and retrieve). Updates to StorageSender---deployed on one chain---are automatically reflected in StorageReceiver---deployed on a different chain. This cross-chain communication is automatically abstracted by Cubist, which means that we can write StorageSender and StorageReceiver as if they're on a single chain. It also means we can change the chain on which these contracts are deployed by changing a single line of configuration.

Here's the StorageSender source:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.16;

import './StorageReceiver.sol';
import "@openzeppelin/contracts/access/Ownable.sol";

contract StorageSender is Ownable { // deployed on one chain

StorageReceiver receiver; // deployed on a different chain
uint256 number;

constructor (uint256 num, StorageReceiver addr) {
number = num;
receiver = addr;
}

function store(uint256 num) public onlyOwner {
number = num;
receiver.store(number); // cross-chain call
}

function inc(uint256 num) public onlyOwner {
number += num;
receiver.store(number); // cross-chain call
}

// ... dec and retrieve
}

Given the source code and target chains for each contract---in this example, Polygon for StorageSender and Ethereum for StorageReceiver---Cubist generates the code that makes cross-chain interaction possible.

How Cubist generates the cross-chain code

To understand how Cubist works, let's first see what the StorageSender to StorageReceiver interaction would look like on a single chain:

StorageSender holds a reference to the StorageReceiver contract. Any time one of StorageSender's functions is invoked, the contract makes a call to StorageReceiver's store function.

When StorageSender and StorageReceiver are deployed on different chains, though, Cubist must generate the cross-chain code that lets the two contracts interact. Since StorageSender (deployed on Polygon in this example) calls StorageReceiver (deployed on Ethereum in this example), Cubist generates a "shim" version of StorageReceiver and deploys it on Polygon, too. The shim's only job is to emit events any time StorageSender calls a StorageReceiver function. The Cubist relayer picks up those events and relays them to the "real" StorageReceiver contract on Ethereum:

Cubist handles shim generation and deployment (almost completely) behind the scenes, automatically; Cubist's relayer service works in concert with the generated shims in order to send function calls from one chain to another.

note

The next Cubist release will support bridge providers beyond the Cubist Trusted Relayer---e.g., Axelar or Layer Zero. You can swap bridge providers just as you swap chains, with a single line in the configuration file. Contract us at [email protected] or on discord if you're interested in bridge provider support!