IBC-Solidity is an IBC implementation in solidity. Many implementations comply with ICS and ibc-go.
This document describes the overview of architecture and key points that we consider the limitations of ethereum and solidity.
The repository has the following structure. core
and apps
directory names basically correspond to ICS.
contracts/
apps/
20-transfer/ … ics-20
commons/ … common contracts/libraries for apps
clients/
IBFT2Client.sol … Light Client for IBFT 2.0 consensus
MockClient.sol … Mock Client
core/
02-client/ … ics-02
03-connection/ … ics-03
04-channel/ … ics-04
24-host/ … ics-24
25-handler/ … ics-25
proto/ … code generated by solidity-protobuf
To relax the contract size limit of ethereum, each ICS implementation is split into IBCClient, IBCConnection, IBCChannel, IBCPacket, and IBCHandler contracts, as shown in the above figure.
In general, such a design causes storage splitting, so it is required to implement unnecessary authentication and accessors for inter-contract calls.
In ibc-solidity, each contract inherits IBCStore contract that defines the common storage layout and uses delegatecall
for contract calls to avoid this.
In IBC, two types of stores are defined: provableStore
and privateStore
. The following are the requirements for each store:
The
provableStore
:
- MUST write to a key/value store whose data can be externally proved with a vector commitment as defined in ICS 23.
- MUST use canonical data structure encodings provided in these specifications as proto3 files
The
privateStore
:
- MAY support external proofs, but is not required to - the IBC handler will never write data to it which needs to be proved.
- MAY use canonical proto3 data structures, but is not required to - it can use whatever format is preferred by the application environment.
In ibc-solidity, we define state variables (e.g. connections, channels) of the type defined with proto3 corresponding to each state. In addition, define a mapping state variable that represents the commitments to satisfy the externally provable property of the provableStore
. For the key of the mapping, keccak256 of the ICS-23 Path
is used, and for the value, keccak256 of the ICS-23 Value
is used.
So how do we get a proof of the commitments state variable? In Solidity, state variables of contracts defined are stored in storage according to this layout spec.
The storage location for each commitment is calculated as follows: assume that the slot of the commitments state variable is s
and a ICS-23 commitment path is p
, the storage location is keccak256(keccak256(p) . s)
.
Also, you can query an existence/non-existence proof of a commitment corresponding to the location using eth_getProof
provided by ethereum execution client. An example go code is here.
You can support any light client via developing a contract that implements ILightClient interface. It also can be registered through registerClient
function in IBCHandler
.
ILightClient
interface includes the following functions:
-
createClient
: Creates a new client with the given state. If successful, it returns a commitment for the initial state. -
updateClient
: Updates the client corresponding to the given clientId. If successful, it returns a commitment for the updated state. If there are no updates for the consensus state, this function returns an empty array of ConsensusStateUpdate objects. -
verifyMembership
: A generic proof verification method that verifies the existence of a value at a given CommitmentPath and height. The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path as defined in ICS-24. -
verifyNonMembership
: A generic proof verification method that verifies the absence of a given CommitmentPath at a specified height. The caller is expected to construct the full CommitmentPath from a CommitmentPrefix and a standardized path as defined in ICS-24.
The registered light client can be instantiated by createClient
, which can be used to verify the handshake process to establish connections and channels.
Different from ibc-go, the light client contract keeps the state in own contract storage in ibc-solidity. This is an optimization based on the high cost of serialization state and read/write storage in solidity. For this reason, createClient
and updateClient
only return a commitment.
Developers can register an App module that implements IIBCModule interface via bindPort
of IBCHandler
. The registered contract becomes available through a channel established by the handshake process with the corresponding port.
The following is a simple App example that only checks that the message sent and the one returned from the destination chain match.
contract EchoApp is IBCAppBase {
IBCHandler private immutable ibcHandler;
constructor(IBCHandler ibcHandler_) {
ibcHandler = ibcHandler_;
}
// An entry point of this app
function sendMessage(
string calldata sourcePort,
string calldata sourceChannel,
uint64 timeoutHeight,
uint64 timeoutTimestamp
) external {
// NOTE: `ibcHandler` checks if the app contract is a valid channel owner
// Otherwise, the function call is reverted
ibcHandler.sendPacket(
sourcePort,
sourceChannel,
Height.Data({revision_number: 0, revision_height: timeoutHeight}),
timeoutTimestamp,
bytes("hello")
);
}
// just return a received message as packet acknowledgement
function onRecvPacket(Packet.Data calldata packet, address relayer) external virtual override onlyIBC
returns (bytes memory acknowledgement)
{
return packet.data;
}
// ensure that the message returned by the packet receiver matches the one sent
function onAcknowledgementPacket(Packet.Data calldata packet, bytes calldata acknowledgement, address relayer) external virtual override onlyIBC
{
require(keccak256(acknowledgement) == keccak256(bytes("hello")));
}
// onlyIBC modifier checks if a caller matches `ibcAddress()`
function ibcAddress() public view virtual override returns (address) {
return address(ibcHandler);
}
}
The packet sending flow using the EchoApp
is the following:
- src chain: send a packet containing "hello" as data qith
sendMessage
of theEchoApp
. - dst chain:
- The relayer submits the packet from 1 with
recvPacket
of theIBCHandler
. - The
IBCHandler
ensure that the packet commitment is valid and callsonRecvPacket
of theEchoApp
. onRecvPacket
returns the received packet data as acknowledgement data
- The relayer submits the packet from 1 with
- src chain:
- The relayer submits the 1. packet and 2. acknowledgement
- The
IBCHandler
ensure that the acknowledgement commitment is valid and callsonAcknowledgementPacket
of theEchoApp
. - In
onAcknowledgementPacket
, ensure that acknowledgement data matches the send message("hello")
Also, an App can define callback functions for state transitions in the channel handshake. See IIBCModule interface for more details.
Further example implementations are ICS-20 implementation and a tutorial that describes e2e packet relay using a small IBC-App called minitoken.