A multi-chain storage app
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
, inc
rement, and dec
rement 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.
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!