HyperbridgeGatewayAdapterLib
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
| Name | Type | Description |
|---|---|---|
| $ | 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
| Name | Type | Description |
|---|---|---|
| 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
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| <none> | address |
stateMachineIdOf
function stateMachineIdOf(uint256 chainId) internal view returns (bytes memory);Parameters
| Name | Type | Description |
|---|---|---|
| chainId | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bytes |
chainIdOfStateMachine
function chainIdOfStateMachine(bytes calldata stateMachineId) internal view returns (uint256);Parameters
| Name | Type | Description |
|---|---|---|
| stateMachineId | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | uint256 |
hyperbridgeRemoteModuleOf
function hyperbridgeRemoteModuleOf(uint256 chainId) internal view returns (bytes memory);Parameters
| Name | Type | Description |
|---|---|---|
| chainId | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <none> | bytes |
hyperbridgeDestTimeoutOf
function hyperbridgeDestTimeoutOf(uint256 chainId) internal view returns (uint64);Parameters
| Name | Type | Description |
|---|---|---|
| chainId | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| _ | bytes4 |
Returns
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| recipient | bytes | |
| payload | bytes | |
| relayerFee | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| chainId | uint256 |
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
| Name | Type | Description |
|---|---|---|
| chainId | uint256 | |
| stateMachineId | bytes |
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
| Name | Type | Description |
|---|---|---|
| chainId | uint256 | |
| module | bytes |
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
| Name | Type | Description |
|---|---|---|
| chainId | uint256 | |
| timeout | uint64 |
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
| Name | Type | Description |
|---|---|---|
| recipient | bytes | |
| payload | bytes | |
| attributes | bytes[] |
Returns
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| recipient | bytes | |
| payload | bytes | |
| relayerFee | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| sendId | bytes32 |
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
| Name | Type | Description |
|---|---|---|
| incoming | IncomingPostRequest |
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
| Name | Type | Description |
|---|---|---|
| request | PostRequest |
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
| Name | Type | Description |
|---|---|---|
| chainId | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| chainId | uint256 | |
| stateMachineId | bytes |
_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
| Name | Type | Description |
|---|---|---|
| $ | HyperbridgeGatewayAdapterStorage | |
| recipient | bytes |
Returns
| Name | Type | Description |
|---|---|---|
| chainId | uint256 | |
| stateMachineId | bytes | |
| module | bytes |
_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
| Name | Type | Description |
|---|---|---|
| $ | HyperbridgeGatewayAdapterStorage | |
| recipient | bytes | |
| payload | bytes | |
| relayerFee | uint256 | |
| nonce | uint256 | |
| sendId | bytes32 |
_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
| Name | Type | Description |
|---|---|---|
| recipient | bytes | |
| payload | bytes | |
| nonce | uint256 |
Returns
| Name | Type | Description |
|---|---|---|
| <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
| Name | Type | Description |
|---|---|---|
| host_ | address | |
| post | DispatchPost |
Returns
| Name | Type | Description |
|---|---|---|
| commitment | bytes32 |