Skip to main content
ICS27GMP is the General Message Passing application. It carries arbitrary call payloads to be executed across chains that implement GMP. A sender reaches it to dispatch a call. The router reaches it to deliver a packet. A received call runs from an ICS27Account that ICS27GMP deploys, so the contract being called sees an address unique to the remote sender. Senders can build on ICS27GMP: IFT is built on GMP to fungibly move tokens across chains. Source: ICS27GMP.sol, with the account contract and the libraries listed below. The model behind this surface is on GMP: how it works. Interacts with: ICS26Router for sends and packet callbacks, its own ICS27Account children, the AccessManager that holds its authority, and every sending contract it calls back, including IFT contracts.

Contracts on this page

One ICS27Account exists per (clientId, sender, salt) triple, so a chain holds one account for every distinct triple that has received a call.

How a call moves through the contracts

A sender starts a call with sendCall, which builds the packet data and hands the packet to the router, which commits a record of it. Once a relayer carries it to the destination chain, the router verifies the packet with the light client and then calls onRecvPacket. ICS27GMP checks the payload’s identity fields, finds or deploys the sender’s account, and asks that account to call the receiver. The target’s return data goes back to the router as the acknowledgement. Before it makes that call, onRecvPacket checks three things:
  • The payload’s version, encoding, and both port identifiers match the ICS27 constants.
  • The payload carries bytes.
  • The receiver string parses as an EVM address, or the call reverts ICS27InvalidReceiver.
Back on the source chain the router calls onAcknowledgementPacket when that acknowledgement arrives, or onTimeoutPacket when the deadline passes without receipt. Either handler recovers the original sender from the packet data and forwards the outcome to that sender’s callback. The three handlers the router calls — onRecvPacket, onAcknowledgementPacket, and onTimeoutPacket — accept the router alone, and revert ICS27Unauthorized for any other caller.

Sending a call

Any contract or externally owned account can call a send.
ICS27GMP fills in the packet’s sender itself, taking msg.sender and formatting it as a checksummed hex string. The returned sequence number, paired with the client it was counted on, is what a sender stores to match later callbacks. Because ICS27GMP inherits Multicall, several sends fit in one transaction, each returning its own. These conditions reject a send:
  • An empty payload reverts ICS27PayloadEmpty.
  • A timeout at or before the current block timestamp, or more than one day out, reverts in the router.
  • An unregistered sourceClient reverts IBCCounterpartyClientNotFound in the router.
  • A paused ICS27GMP rejects the send.
The router accepts a packet on gmpport from ICS27GMP alone, so no other contract can send something that arrives as a GMP call.

Packet data on the wire

The packet data is an ABI-encoded GMPPacketData, and the payload carrying it uses fixed port, version, and encoding values.
ICS27GMP builds that struct from the send message. It adds the sender, while the client identifier and the timeout become fields of the packet rather than of its data. The payload around it carries three more fields, fixed by constants in ICS27Lib and identical in both directions. The memo travels in the packet data, and no handler reads it.

Destination accounts and their addresses

A destination account is the contract that makes a remote sender’s call on this chain. It is identified by client, sender, and salt. It is deployed on first use at a CREATE2 address, which is computable in advance. ICS27GMP never makes the destination call itself. It asks the sender’s account contract to make the low-level call, so the target sees a stable sender-specific msg.sender rather than the GMP contract.
The salt is the sender’s own choice, and changing it changes the identifier. One sender can drive several accounts on the same chain by choosing different salts. An empty salt is allowed, and gives the sender one default account per client. An account is deployed on the first inbound call for its identifier. ICS27GMP hashes the identifier, then uses that hash as the CREATE2 salt for a beacon proxy it deploys itself.
The address therefore depends on all three identifier fields, on the account beacon, and on ICS27GMP’s own address, which the proxy bytecode and the CREATE2 call both carry. The same identifier under a different ICS27GMP deployment resolves to a different address. Two views expose the mapping in both directions. This allows a sender to compute a destination address before creation and fund or authorize its destination account ahead of its first call. The reverse lookup is how a destination contract tells which remote sender is calling it. IFT uses it in iftMint, checking the calling account’s sender and salt before minting.

The account contract’s functions

Only ICS27GMP can drive an account from outside, and it has one function for the purpose. The account’s other primitives answer to the account alone. An inbound payload runs through functionCall, which calls the target like OpenZeppelin’s Address.functionCall but fails the relay when the call most likely ran out of gas (see Gas and value). The four value and batching primitives carry an onlySelf check, so msg.sender has to be the account. A sender reaches them by addressing a GMP call to its own account, with the primitive as the payload. A batch is an array of Call { target, data, value }. Value has to be in the account already, because a GMP call carries none. Because addresses are pre-computable, a sender can fund its account before its first call ever arrives.

Acknowledgements and sender callbacks

Results reach a sending contract only if it implements the callback interface and advertises it through ERC165. An acknowledgement takes one of two shapes. Success is abi.encode(GMPAcknowledgement{ result }), carrying the target’s raw return data. Failure is the fixed 32-byte universal error acknowledgement, sha256("UNIVERSAL_ERROR_ACKNOWLEDGEMENT"). To hear either one, a sending contract implements IIBCSenderCallbacks.
The success flag is not carried in the acknowledgement. ICS27GMP derives it by hashing the acknowledgement bytes and comparing them against the universal error value, so anything that is not that value counts as success. IBCSenderCallbacksLib calls back only a sender that answers the ERC165 check, and skips any other in silence. A missing interface therefore costs the sender its callback, not the packet. The acknowledgement struct carries the source and destination client identifiers, the packet sequence, the full original payload, the raw acknowledgement bytes, and the relayer address. The timeout struct is the same one minus the acknowledgement. Both callbacks fire synchronously inside the relayer’s ackPacket or timeoutPacket transaction, after the packet commitment is deleted.

Gas and value

The destination call runs on whatever gas the relayer’s transaction leaves, and a GMP call carries no native value. The account forwards the destination call with a low-level call that sends all remaining gas, as OpenZeppelin’s Address.functionCall does. Gas budgeting is delegated to the relayer. The account bubbles up a failed call’s revert data and turns an empty revert into a FailedCall error, so the router writes the error acknowledgement and the packet is consumed. The exception is an empty revert that leaves the account less than an eighth of the gas it had before the call: the target most likely ran out of gas, up to about eight frames below it, so the account reverts without data too. The router rejects empty revert data outright with IBCFailedCallback, as it does when the callback frame itself dies, so the relay fails and the packet stays in flight. A relayer can then retry that packet with more gas. Packets carry no gas limit, so a relayer can still starve a target that turns running out of gas into an error of its own, swallows it, or checks gasleft() itself. A GMP call carries call data alone, so any native value a target receives has to be in the account already.

State and storage

The contract stores the account registry in both directions and keeps nothing per packet. Storage is four fields, held in one ERC-7201 namespaced slot:
Nothing in the contract deletes any of it, so the account registry only grows. Per-packet state lives elsewhere. GMP keeps no record to clean up, and the commitment and the receipt live in the router’s store.

Initialization and registration

The proxy is initialized once with the router, the account logic, and the authority. A separate call then registers it on the router under the port gmpport. ICS27GMP is deployed as a logic contract behind an ERC1967 proxy, and its initializer takes three addresses:
That call also creates the ICS27Account beacon from accountLogic. The router address and the account beacon are fixed from that point on, each assigned only in initialize and with no setter. Both are readable through the ics26() and getAccountBeacon() views. ICS27GMP serves every sender on the chain under one port, gmpport. Registration goes through the router’s restricted addIBCApp(string, address), the overload that takes a custom identifier rather than an address-shaped one. The caller is an external account the access manager permits. On a chain ibc deploy gmp brings up, that is the deploy key. Permissions and upgrades covers which role holds that selector. A port can be claimed only once, and a second attempt reverts IBCPortAlreadyExists. One ICS27GMP per IBC deployment keeps every sender on a chain mutually reachable, since a second one would claim its own port and derive its own account addresses.

Events and errors

ICS27GMP declares no events, so a call is observed through the router’s. Six router events cover a call’s life. The revert reason from a failed destination call appears in IBCAppRecvPacketCallbackError alone. It stays on the destination chain, so diagnosing a failure means reading destination logs. Both addresses still emit inherited events. The ICS27GMP address emits pause, upgrade, initialization, and authority events. An account address emits initialization. The nine errors are declared in IICS27Errors. The receiver and sender errors surface in different places. An unparseable receiver reverts the receive, which the router turns into an error acknowledgement. An unparseable sender reverts acknowledgement or timeout processing instead.

Roles, pausing, and upgrades

An AccessManager gates the pause switch and every upgrade. One upgrade call replaces the logic behind every account at once. Which of those roles a chain actually assigns is the deployment’s choice. Pausing reaches all four packet-facing entry points, each of them whenNotPaused:
  • New sends halt.
  • On the destination, inbound calls become error acknowledgements through the router’s catch.
  • Acknowledgements and timeouts revert until the contract is unpaused.
ICS27GMP itself is UUPS-upgradeable, with a restricted _authorizeUpgrade. Accounts upgrade through upgradeAccountTo, which replaces the implementation behind the account beacon rather than repointing the stored beacon address. That reaches every account at once, since all account proxies read the same UpgradeableBeacon that ICS27GMP owns. The Permissions and upgrades page carries the full proxy map and the role assignments each deployment makes.