Skip to content
Lattice

HyperbridgeGatewayAdapterLib

Git Source

Title: HyperbridgeGatewayAdapterLib

Authors: David Dada <daveproxy80@gmail.com> (https://github.com/dadadave80), Modified from Hyperbridge (https://github.com/polytope-labs/ismp-solidity)

Logic + ERC-7201 storage for the Hyperbridge (ISMP) ERC-7786 gateway adapter. sendMessage wraps the ERC-7930 envelope with a source-minted nonce and dispatches a proof-verified POST request via IIsmpDispatcher.dispatch, paying the per-byte protocol fee + optional relayer fee in the host's ERC-20 feeToken() (CCTP-style pull/approve/reset hygiene — no msg.value); onAccept is the host-gated delivery callback (source state machine + trusted module auth, (source, protocol-nonce) dedup, ERC-7930 delivery); onPostRequestTimeout is the native timeout notification.

Wire envelope = abi.encode(senderInteropAddr, recipientInteropAddr, innerPayload, nonce) (the suite 4-tuple). State machine ids are BYTES strings: for EVM chains the id is DERIVED as bytes("EVM-" + Strings.toString(chainId)) — upstream StateMachine.evm re-implemented here in one line rather than vendoring StateMachine.sol; non-EVM ids (POLKADOT-/SUBSTRATE-) are admin-supplied raw. REFUND-CRITICAL: DispatchPost.payer is ALWAYS the sending user (msg.sender), NEVER address(this) — the host refunds timeout fees to the payer, and a diamond payer would strand them. The host's feeToken() is read LIVE on every send/quote (the host can migrate it — caching would charge/approve the wrong token). A native-token fee path (host-side uniswap swap of msg.value) exists upstream but is DEFERRED — stray value reverts in v1 (issue #77 Q13).

Functions

hyperbridgeGatewayAdapterStorage

function hyperbridgeGatewayAdapterStorage() internal pure returns (HyperbridgeGatewayAdapterStorage storage $);

Returns

NameTypeDescription
$HyperbridgeGatewayAdapterStorage

__HyperbridgeGatewayAdapter_init

Stores the IsmpHost (zero reverts HyperbridgeZeroHost — an unconfigured adapter must not exist) and registers the gateway-source + adapter ERC-165 ids.

function __HyperbridgeGatewayAdapter_init(address host_) internal;

Parameters

NameTypeDescription
host_address

registerInterfaces

Writes true to the SHARED IERC7786GatewaySource ERC-165 map slot (0x11967553 → ...; same slot the CCIP/LayerZero/Wormhole/Axelar/Hyperlane adapters register — a Diamond mounts at most one gateway) and to the adapter's own IHyperbridgeGatewayAdapter map slot.

function registerInterfaces() internal;

ismpHost

function ismpHost() internal view returns (address);

Returns

NameTypeDescription
<none>address

hyperbridgeFeeToken

LIVE passthrough of the host's current ERC-20 fee token — deliberately never cached (the host can migrate it, and a cached token would charge/approve the wrong asset).

function hyperbridgeFeeToken() internal view returns (address);

Returns

NameTypeDescription
<none>address

stateMachineIdOf

function stateMachineIdOf(uint256 chainId) internal view returns (bytes memory);

Parameters

NameTypeDescription
chainIduint256

Returns

NameTypeDescription
<none>bytes

chainIdOfStateMachine

function chainIdOfStateMachine(bytes calldata stateMachineId) internal view returns (uint256);

Parameters

NameTypeDescription
stateMachineIdbytes

Returns

NameTypeDescription
<none>uint256

hyperbridgeRemoteModuleOf

function hyperbridgeRemoteModuleOf(uint256 chainId) internal view returns (bytes memory);

Parameters

NameTypeDescription
chainIduint256

Returns

NameTypeDescription
<none>bytes

hyperbridgeDestTimeoutOf

function hyperbridgeDestTimeoutOf(uint256 chainId) internal view returns (uint64);

Parameters

NameTypeDescription
chainIduint256

Returns

NameTypeDescription
<none>uint64

supportsAttribute

No sendMessage attributes are supported by this adapter (the relayer fee rides the separate typed sendMessageWithFee entrypoint, not an attribute).

function supportsAttribute(bytes4) internal pure returns (bool);

Parameters

NameTypeDescription
_bytes4

Returns

NameTypeDescription
<none>bool

quoteDispatchFee

Quotes the TOTAL feeToken cost of a send: perByteFee(dest) * body.length + relayerFee, where body is the exact wire envelope a real send would dispatch (SAME builder, with the NEXT nonce — envelope length is sender/nonce-independent, so the quote matches the send-time charge). The host's 32-byte minimum-body floor never binds: the envelope is always ≥ 4 ABI words.

function quoteDispatchFee(bytes calldata recipient, bytes calldata payload, uint256 relayerFee)
        internal
        view
        returns (uint256);

Parameters

NameTypeDescription
recipientbytes
payloadbytes
relayerFeeuint256

Returns

NameTypeDescription
<none>uint256

registerStateMachine

Registers the EVM chainId ⇄ state machine equivalence for chainId, deriving the id INTERNALLY as evmStateMachineId — never caller-supplied (fail-closed: a typoed id cannot misroute). FAIL-LOUD identity admin (both directions, via the keccak reverse map). Admin only.

function registerStateMachine(uint256 chainId) internal;

Parameters

NameTypeDescription
chainIduint256

registerStateMachineRaw

Registers a RAW (non-EVM: POLKADOT-/SUBSTRATE-/KUSAMA-) state machine id under a LOCAL routing chainId handle, with empty-id rejection and the same fail-loud both-direction rules. Admin only.

function registerStateMachineRaw(uint256 chainId, bytes calldata stateMachineId) internal;

Parameters

NameTypeDescription
chainIduint256
stateMachineIdbytes

registerRemoteModule

Sets the trusted remote ISMP module (counterpart adapter) for a chain. TUNABLE (updatable), but never empty — clearing a corridor is not supported in v1. Admin only.

function registerRemoteModule(uint256 chainId, bytes calldata module) internal;

Parameters

NameTypeDescription
chainIduint256
modulebytes

configureDestinationTimeout

Configures a destination's dispatch timeout in seconds. TUNABLE; 0 = no explicit timeout (the host's default handling). Admin only.

function configureDestinationTimeout(uint256 chainId, uint64 timeout) internal;

Parameters

NameTypeDescription
chainIduint256
timeoutuint64

sendMessage

ERC-7786 source. Delegates to sendMessageWithFee with relayerFee = 0 (unfunded/self-relay, valid in ISMP — anyone may still relay the proof permissionlessly).

No attributes are supported — any attribute reverts UnsupportedAttribute. The interface forces payable, but the native-token fee path is DEFERRED (issue #77 Q13) — stray value reverts HyperbridgeUnexpectedValue instead of being trapped in the Diamond.

function sendMessage(bytes calldata recipient, bytes calldata payload, bytes[] calldata attributes)
        internal
        returns (bytes32);

Parameters

NameTypeDescription
recipientbytes
payloadbytes
attributesbytes[]

Returns

NameTypeDescription
<none>bytes32

sendMessageWithFee

Sends payload to the ERC-7930 recipient, funding an ISMP relayer fee of relayerFee on top of the per-byte protocol fee — both charged in the host's LIVE feeToken() and pulled from the caller with CCTP-style hygiene: pull exactly total, force-approve the host for exactly total, dispatch, reset the allowance to 0, then delta-sweep any leftover back to the caller (defensive — the host should pull exactly total).

The envelope carries a source-owned monotonic nonce (++$._lastNonce) so the delivery id is globally unique per (source, nonce) — the ISMP protocol nonce is host-side and unknowable pre-dispatch, so the envelope nonce keeps the suite-level identity every sibling adapter uses. REFUND-CRITICAL: DispatchPost.payer = msg.sender (the USER) — a diamond payer would strand host-side timeout fee refunds. Returns sendId = keccak256(abi.encode(block.chainid, nonce)); the ISMP commitment is surfaced via HyperbridgeMessageDispatched.

function sendMessageWithFee(bytes calldata recipient, bytes calldata payload, uint256 relayerFee)
        internal
        returns (bytes32 sendId);

Parameters

NameTypeDescription
recipientbytes
payloadbytes
relayerFeeuint256

Returns

NameTypeDescription
sendIdbytes32

onAccept

Host delivery callback: DUAL AUTH — msg.sender must be the IsmpHost AND request.from (the origin module) must byte-equal the non-empty trusted remote registered for the source state machine's chain (fail-closed on an unregistered source or an unset remote). De-dups per (chainId, keccak256(abi.encode(source, protocolNonce))), marking BEFORE the external delivery (checks-effects-interactions), then delivers to the ERC-7930 recipient encoded in the envelope.

The dedup key uses the ISMP PROTOCOL nonce — host-side unique per source — as suite-convention defense-in-depth over the host's own commitment uniqueness (the host never redelivers a settled commitment; this guard makes the adapter safe even against a buggy host re-invocation). nonReentrant: delivery calls an arbitrary recipient.

function onAccept(IncomingPostRequest calldata incoming) internal;

Parameters

NameTypeDescription
incomingIncomingPostRequest

onPostRequestTimeout

Host timeout callback for a request WE dispatched: decodes the envelope and emits HyperbridgeRequestTimedOut. EMIT-ONLY in v1 — the fee refund itself is HOST-SIDE to DispatchPost.payer (the original sending user); this hook is the notification surface an application layer keys compensating actions off (issue #77 Q8).

A timeout fires on the SOURCE chain for our own dispatch, so request.source is OUR state machine and request.from is OUR module — neither is attacker-controlled once msg.sender == host holds, which is the REAL auth here (the host only ever times out commitments it stored at dispatch). sendId = keccak256(abi.encode(block.chainid, envelopeNonce)) reconstructs the original send id because block.chainid equals the send-time source chainId. nonReentrant for symmetry with onAccept (the emit path makes no external calls, but the guard keeps the hook surface uniform).

function onPostRequestTimeout(PostRequest calldata request) internal;

Parameters

NameTypeDescription
requestPostRequest

unsupportedHook

The four IIsmpModule hooks the adapter never uses (onPostResponse, onGetResponse, onPostResponseTimeout, onGetTimeout) revert unconditionally — the adapter dispatches neither GETs nor responses, so silently accepting them would be a spoofable no-op surface.

function unsupportedHook() internal pure;

evmStateMachineId

The canonical ISMP state machine id of an EVM chain: bytes("EVM-" + decimal chainId).

One-line re-implementation of upstream StateMachine.evm (ismp-solidity StateMachine.sol) with the repo's vendored Strings — deliberately not vendoring the whole StateMachine lib.

function evmStateMachineId(uint256 chainId) internal pure returns (bytes memory);

Parameters

NameTypeDescription
chainIduint256

Returns

NameTypeDescription
<none>bytes

_registerStateMachine

Registers chainId ⇄ stateMachineId in BOTH directions. Reverts HyperbridgeZeroChainId on chainId 0 (0 is the reverse-map unset sentinel) and fails LOUD (HyperbridgeStateMachineAlreadyRegistered) when either direction is already mapped — identities are never remapped.

function _registerStateMachine(uint256 chainId, bytes memory stateMachineId) private;

Parameters

NameTypeDescription
chainIduint256
stateMachineIdbytes

_resolveDestination

Resolves recipient's chain into (chainId, state machine id, trusted remote module).

Reverts HyperbridgeUnknownDestinationChain if the state machine id or remote module is unset — fail-closed, mirroring the Hyperlane _resolveDestination. The eip-155 recipient parse is the cross-check: routing keys off the REGISTERED map for the recipient's own chainId, never off caller-supplied ids.

function _resolveDestination(HyperbridgeGatewayAdapterStorage storage $, bytes calldata recipient)
        private
        view
        returns (uint256 chainId, bytes memory stateMachineId, bytes memory module);

Parameters

NameTypeDescription
$HyperbridgeGatewayAdapterStorage
recipientbytes

Returns

NameTypeDescription
chainIduint256
stateMachineIdbytes
modulebytes

_dispatch

Resolves the destination, builds the DispatchPost (payer = the USER — REFUND-CRITICAL: host-side timeout fee refunds go to the payer, and a diamond payer would strand them), runs the fee-hygiene dispatch, and emits HyperbridgeMessageDispatched.

function _dispatch(
        HyperbridgeGatewayAdapterStorage storage $,
        bytes calldata recipient,
        bytes calldata payload,
        uint256 relayerFee,
        uint256 nonce,
        bytes32 sendId
    ) private;

Parameters

NameTypeDescription
$HyperbridgeGatewayAdapterStorage
recipientbytes
payloadbytes
relayerFeeuint256
nonceuint256
sendIdbytes32

_buildEnvelope

The suite 4-tuple wire envelope: abi.encode(sender7930, recipient7930, payload, nonce). Shared by sendMessageWithFee and quoteDispatchFee so the quoted body IS the dispatched body.

function _buildEnvelope(bytes calldata recipient, bytes calldata payload, uint256 nonce)
        private
        view
        returns (bytes memory);

Parameters

NameTypeDescription
recipientbytes
payloadbytes
nonceuint256

Returns

NameTypeDescription
<none>bytes

_pullAndDispatch

CCTP-style ERC-20 fee hygiene around the ISMP dispatch: read the LIVE feeToken, pull exactly total = perByteFee(dest) * body.length + relayerFee from the caller, force-approve the host for exactly total, dispatch, reset the allowance to 0, then delta-sweep any leftover feeToken back to the caller (defensive — the host should pull exactly total; a pre-existing Diamond balance is never swept because only the balance DELTA is).

function _pullAndDispatch(address host_, DispatchPost memory post) private returns (bytes32 commitment);

Parameters

NameTypeDescription
host_address
postDispatchPost

Returns

NameTypeDescription
commitmentbytes32