ICS26Router contract is the core router of IBC-solidity. Every packet in either direction passes through it. It validates each call and records the outcome in its own store. It works with light clients to verify proofs, and it forwards payloads to applications.
Source: ICS26Router.sol. The protocol model behind this surface is on Core: router and store and Packet lifecycle.
What the router does
The router owns the application registry, the light client registry, and the commitment store. It drives the whole packet lifecycle of send, receive, acknowledge, and timeout. Registered applications callsendPacket, and relayers call recvPacket, ackPacket, timeoutPacket, and updateClient. Those relayer calls are gated, and one external OpenZeppelin AccessManager decides who passes. The same manager decides who may pause registration, which never stops the packet lifecycle. The router calls back into applications through IIBCApp, and it hands every cross-chain claim to a light client through ILightClient.
Interacts with: AttestationLightClient, BesuLightClient, ICS27GMP and accounts, IFT contracts, and Permissions and upgrades.
Contracts on this page
Registering an application
Every IBC application is registered on a port, and implements three router-only callbacks. A port belongs permanently to the first application registered on it.
The role map names
ID_CUSTOMIZER_ROLE for the custom-port function. The port that function picks must pass identifier validation, and it must not parse as an address, so a chosen name can never collide with one derived from a contract address. Custom identifiers may not start with client- or channel-, so a chosen name can never collide with a generated one.
Either function reverts if the port already exists, and while the router is paused.
To be an IBC application, a contract must implement IIBCApp, which defines three callbacks:
Sending a packet
The router acceptssendPacket only from the application registered on the payload’s source port. After accepting it, it fills in the rest of the packet fields.
- The caller supplies the source client, a timeout, and one payload. The router checks
msg.senderagainst the application registry before anything else. - Then, the router fills in the other two fields: the destination client, from the counterparty registered for the source client, and the sequence, from a per-client counter that starts at 1 and increments on every send. The call returns that sequence as a
uint64. - The timeout must be in the future and no more than
MAX_TIMEOUT_DURATIONaway, a constant of one day, both bounds measured against the sending chain’s own block timestamp. - The call leaves a 32-byte commitment at the packet’s path. The full packet reaches a relayer through the
SendPacketevent.
Relayer entry points
These are the packet-lifecycle calls a relayer makes to the router. Each carries a proof, and the router hands that proof to a light client to check. A relayer also callsupdateClient. That call is in Client and counterparty registry.
The router also inherits OpenZeppelin’s
multicall, which batches several calls into one transaction. The IBC CLI relayer batches through a separate IBCMulticall contract instead, which calls the router in order: it puts an updateClient call ahead of the packet calls it is relaying, so the client holds the proof height before any verification runs.
Each takes the reconstructed packet, one proof, and the height that proof was taken at. ackPacket also carries the acknowledgement bytes.
Acknowledgements and callback failures
A receiving application returns its acknowledgement in the same transaction, and the router commits what comes back. An application that reverts with a reason still leaves an acknowledgement on chain. The router catches the revert, emits the reason, and commitssha256("UNIVERSAL_ERROR_ACKNOWLEDGEMENT") from ICS24Host in its place.
The failures differ in what becomes of the packet. A revert with a reason settles it, because an acknowledgement reaches the source chain. A revert with no reason leaves it pending, for a relayer to retry with more gas. A port with no application registered leaves it unreceivable, and its only ending is a timeout. An application that returns empty acknowledgement bytes, or the reserved universal error acknowledgement, leaves it unreceivable in the same way. Those reverts repeat on retry, so a timeout ends the packet. The reverts themselves are in Errors.
Client and counterparty registry
The router carries the client registry it inherits from ICS02ClientUpgradeable. Registering a client, keeping one fresh, and replacing one are all role-gated.
Two things are stored per client identifier: the address of a light-client contract, and counterparty info, which is the counterparty’s own client identifier plus its merkle prefix. The generated-identifier overload builds the identifier as
client- plus a counter. The custom-identifier overload lets an ID customizer supply one instead. Both overloads revert while the router is paused.
Misbehaviour submission is open to any address, and the router forwards the evidence to the client registered under the identifier. What comes of it is the client’s decision. A client that has gone wrong is replaced under its own identifier: an admin-gated migrateClient swaps in a new client contract and counterparty info.
To be registrable, a client contract implements five functions: updateClient, verifyMembership, verifyNonMembership, misbehaviour, and getClientState. Both verify functions take an opaque proof, a proof height, and a merkle path. Membership also takes the value it expects to find there. Both return the counterparty’s unix timestamp at that height. Clients are interchangeable behind that interface, such as the AttestationLightClient. IBC-solidity’s own light clients are also bound at construction to one router and accept calls from it alone, so each is registered on the router it was deployed for. A client pauses separately from the router; see Permissions on a light client.
Pausing registration
The router can pause registration and nothing else.pause and unpause are role-gated, and the role map names PAUSER_ROLE and UNPAUSER_ROLE for them. While the router is paused, both forms of addIBCApp and both forms of addClient revert with OpenZeppelin’s EnforcedPause. Sending, receiving, acknowledging, and timing out packets, updateClient, both verify functions, submitMisbehaviour, and migrateClient all keep working.
The pause flag sits in its own storage namespace and starts unpaused, so a router upgraded from a release without pausing needs no migration. Its new pause and unpause selectors answer to the admin until an admin assigns them to the pause roles.
Commitment storage and queries
A commitment is a hash of something the chain did, stored so the counterparty can verify it happened without being handed the original. Each one lives in the same mapping, from a hashed path to a fixed-length value, and the path is what says which kind it is.IBCStoreUpgradeable holds that mapping alongside the per-client send counter.
Each kind commits a different value.
- Each payload hash inside a packet’s value covers the source port, the destination port, the version, the encoding, and the value.
- A receipt value must be non-zero, which is what lets non-membership prove a packet was never received. The code relies on that property rather than deriving it.
getCommitment(bytes32 hashedPath), which returns the stored value, or zero. The key is keccak256 of the raw path in the table above. RelayerHelper wraps that call: queryPacketCommitment, queryPacketReceipt, and queryAckCommitment build the path from a client identifier and a sequence, and isPacketReceived and isPacketReceiveSuccessful take the packet itself. The latter is true only for a received packet whose stored acknowledgement is not the universal error acknowledgement.
Events
- Every packet event indexes a client identifier and the sequence. Which client is indexed depends on the leg:
SendPacket,AckPacket, andTimeoutPacketindex the source client, andWriteAcknowledgementindexes the destination client.
Errors
The IBC-defined reverts a router call can produce come from one of three places: the router itself, the commitment store with its path library, or the client registry.Roles and permissions
Each gate below is a function selector granted to a role in an external OpenZeppelinAccessManager.
Authorization itself happens outside the router. One external
AccessManager is passed as authority at initialization, and a deployment decides which selectors it binds to which role. The router only asks the access manager whether the caller holds the role assigned to the selector, so who may relay is a deployment decision rather than a property of the contract.
On a chain the IBC CLI brings up, any address may relay.
ibc deploy core binds the router’s relaying calls to the public role, and it never grants the relayer role. It leaves addIBCApp(address), the generated-identifier addClient, pause, and unpause unassigned, so only the admin may register an application or a client, or pause registration. The selectors it binds are listed under what a deployment assigns.Deployment and upgrade
The router ships as a logic contract behind an ERC1967 proxy. Its initializer,initialize(authority), takes one argument and binds the AccessManager address behind every gate above.
Upgrades are UUPS with a restricted _authorizeUpgrade. The upgrade selector belongs to the AccessManager admin by default. For the full map across every contract, see Permissions and upgrades.