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 withsendCall, 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
receiverstring parses as an EVM address, or the call revertsICS27InvalidReceiver.
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
sourceClientrevertsIBCCounterpartyClientNotFoundin the router. - A paused ICS27GMP rejects the send.
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-encodedGMPPacketData, and the payload carrying it uses fixed port, version, and encoding values.
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-specificmsg.sender rather than the GMP contract.
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 isabi.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.
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’sAddress.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 portgmpport.
ICS27GMP is deployed as a logic contract behind an ERC1967 proxy, and its initializer takes three addresses:
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.
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.