Skip to content

feat: add Hyperlane verification rail (Base, Arbitrum, Stable) - #65

Draft
reednaa wants to merge 2 commits into
mainfrom
feat/hyperlane-verification-rail
Draft

feat: add Hyperlane verification rail (Base, Arbitrum, Stable)#65
reednaa wants to merge 2 commits into
mainfrom
feat/hyperlane-verification-rail

Conversation

@reednaa

@reednaa reednaa commented Aug 7, 2026

Copy link
Copy Markdown
Member

Adds Hyperlane as a second, independent cross-chain verification rail alongside Polymer. Live and proven end to end on mainnet over Base ↔ Arbitrum — both via an operator script and through this UI — and wired for Stable (988).

Why this is not just "add another oracle"

Hyperlane is push-based, unlike Polymer's pull model. After filling, the solver itself calls submit() on the output chain's oracle, paying interchain gas, and an independent Hyperlane relayer then delivers handle() on the input chain. There is no proof to fetch; the wait is for a third party we do not control, and Hyperlane publishes no SLA.

More importantly, Hyperlane inverts output.oracle relative to Polymer:

Field Hyperlane Polymer
order.inputOracle input chain's oracle input chain's oracle
output.oracle output chain's oracle (a different address) input chain's oracle

@lifi/intent already selects correctly for non-polymer verifiers (buildMandateOutputs falls through to getOracle(verifier, token.chainId)), so no package bump is required for oracle selection. allowedOutputOracles keeps the two rules side by side rather than generalising one into the other.

Pre-existing bugs this fixes

Three of these affected the Polymer path too, and are not Hyperlane-specific:

  1. Proof payloads were missing the mandatory 4-byte FILL_MAGIC (0xd1252dff) that the deployed settlers require. Every isProven() query therefore hashed a payload that could never match, so the on-chain "proven"/validation indicators could never turn green. Two call sites were affected — flowProgress.ts and ReceiveMessage.svelte. A new local encoder src/lib/libraries/fillPayload.ts owns the wire format, pinned by golden vectors generated from the Solidity implementation. Upstream @lifi/intent@0.2.1 is still wrong; a separate PR fixes it there (lifinance/intent.ts), and this local encoder is what unblocks us meanwhile.
  2. The verifier <select> had no bind:value, so store.verifier was permanently "polymer" and the control was inert. Masked because the only other option was disabled.
  3. The terminal-status check sat behind if (allValidated), making the refund case unreachable — a refund is precisely the case where validation did not land in time.
  4. Claimed and Refunded were both reported as "finalised", in flowProgress.ts and FlowStepTracker.svelte. With an asynchronous relay this is financially material: refunded-after-fill is a solver loss, not a success.

A route bug exposed by Stable

Stable is the first Hyperlane-only chain (no Polymer deployment). allowedOutputOracles only required the input chain to have a Polymer oracle — because for Polymer output.oracle is the input chain's oracle — so a Base→Stable order built with the Polymer verifier validated successfully and could then never prove: nothing on Stable observes the fill. Silently offered, permanently stuck. It now requires a Polymer oracle on the output chain as well, and the UI disables the unsupported verifier per route.

Relay-state correctness

Hyperlane submissions are now persisted (new hyperlane_submissions table), which fixes three things that a first pass got wrong:

  • The relay state distinguishes filled-but-not-yet-submitted from awaiting-relayer, so the UI no longer claims "Relaying" before anything was dispatched (or when the submit failed).
  • The Hyperlane message id is parsed from the submit receipt and surfaced with an explorer link, so "is the relayer slow or did delivery revert?" is answerable from the app.
  • The duplicate-payment guard was in-memory only, so a page reload — the natural reaction to a stuck step — would dispatch a second message and pay interchain gas again.

Order expiry is surfaced during the relay wait: if the relayer misses order.expires, the solver has irreversibly paid the output while anyone can refund the user's input, after which finalise reverts. This is informational only — no blocking policy was added.

Scope

Hyperlane is deliberately gated to the escrow / local-demo issuance path. lifi-order-service has no Hyperlane oracle rows, so exposing it on the API-backed compact path would create a half-supported flow.

Verification

  • svelte-check: 0 errors, 0 warnings.
  • Unit tests: 159 pass. Includes golden-vector tests asserting the encoder reproduces all 8 Solidity-generated vectors byte-for-byte (payload and hash, both fill and not-filled kinds).
  • tests/e2e/escrow-hyperlane-blackbox.spec.ts (keyless, no transactions) passes, and is mutation-tested — swapping the two pinned oracle addresses fails it.
  • Mainnet, both directions: orders reached orderStatus = 2 (Claimed) with relay latency ~64–100s. Interchain gas ≈3.79e13 wei Arb→Base and ≈6.82e13 wei Base→Arb.
  • Mainnet through this UI: tests/e2e/escrow-hyperlane-live.spec.ts passed; order 0x9758237c…bedc confirmed Claimed on Base.

Reviewer notes

  • The live spec is double-gated (E2E_PRIVATE_KEY and E2E_RUN_LIVE_HYPERLANE=1) because a bare npm test sweeps the whole e2e directory and would spend real funds. Do not pass --retries — a retry after a successful submit re-pays interchain gas.
  • Quotes must come from the oracle's quoteGasPayment, never the IGP directly: because hook() is zero the Mailbox applies defaultHook plus requiredHook, and the bare IGP quote under-reports by ~2.4x, which would revert dispatch.
  • The destination gasLimit is 80_000 + 40_000 * numPayloads (120k for a single output), from measured handle cost of ~28k fixed + ~27k per payload. Hyperlane's 50,000 default does not cover even one payload.
  • Known caveat on Stable: the gas token reports 18 decimals under effectively the same USDT0 symbol as a real 6-decimal ERC-20 on the chain — two assets, 10^12 apart. Both config sites carry a comment. Use usdt0 for output-side Stable demos, since supportsNativeOutput() is Tron-only (a pre-existing limitation that also affects native ETH elsewhere).

🤖 Generated with Claude Code

Adds Hyperlane as a second, independent cross-chain verification rail
alongside Polymer, live on Base <-> Arbitrum and wired for Stable (988).
Proven end to end on mainnet through this UI.

Hyperlane is push-based, unlike Polymer's pull model: after filling, the
solver itself calls submit() on the OUTPUT chain's oracle paying interchain
gas, then an independent relayer delivers handle() on the input chain.

Critically, Hyperlane inverts output.oracle relative to Polymer:
  order.inputOracle = the INPUT chain's oracle
  output.oracle     = the OUTPUT chain's oracle (a DIFFERENT address)
Polymer uses the input chain's oracle for both. @lifi/intent already
selects correctly for non-polymer verifiers, so no package bump is needed
for oracle selection.

Fixes four pre-existing defects found along the way, three of which
affected the Polymer path too:

- Proof payloads were missing the mandatory 4-byte FILL_MAGIC domain tag
  (0xd1252dff) that the live settlers require, so every isProven() query
  hashed a payload that could never match and the on-chain "proven"
  indicators could never turn green. Two call sites were affected:
  flowProgress.ts and ReceiveMessage.svelte. A new local encoder
  (fillPayload.ts) owns the wire format, pinned by golden vectors
  generated from the Solidity implementation. Upstream @lifi/intent 0.2.1
  is still wrong; a separate PR fixes it there.
- The verifier <select> had no bind:value, so store.verifier was
  permanently "polymer" and the control was inert. Masked because the only
  other option was disabled.
- The terminal-status check sat behind `if (allValidated)`, making the
  refund case unreachable - a refund is precisely the case where
  validation did not land in time.
- Claimed and Refunded were both reported as "finalised". With an
  asynchronous relay this is financially material: refunded-after-fill is
  a solver loss, not a success.

Also fixes a route bug exposed by Stable being the first Hyperlane-only
chain: allowedOutputOracles only required the INPUT chain to have a
Polymer oracle, so a Base->Stable order built with the Polymer verifier
validated successfully and could then never prove. It now requires a
Polymer oracle on the output chain as well.

Hyperlane submissions are persisted (new hyperlane_submissions table), so
the relay state distinguishes filled-but-not-submitted from
awaiting-relayer, the Hyperlane message id is surfaced with an explorer
link, and a page reload no longer loses the duplicate-payment guard and
pay interchain gas twice. Order expiry is surfaced during the relay wait,
since a relay that misses expiry costs the solver its fill.

Hyperlane is deliberately gated to the escrow/local-demo issuance path:
lifi-order-service has no Hyperlane oracle rows, so exposing it on the
API-backed compact path would create a half-supported flow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 998dc232-13c2-4fa4-bc6a-ca970e593034

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🚀 Preview deployed!

Worker: lintent-pr-65
URL: https://lintent-pr-65.li-fi374.workers.dev

Keeps the "Order expires in X" countdown and the post-expiry warning, but
removes the explanatory paragraph about Hyperlane publishing no delivery
SLA and inputs becoming refundable while the output is already paid.

The countdown itself carries the actionable information; the prose was
restating rail mechanics that belong in the docs, not in a demo UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

github-actions Bot commented Aug 7, 2026

Copy link
Copy Markdown

🚀 Preview deployed!

Worker: lintent-pr-65
URL: https://lintent-pr-65.li-fi374.workers.dev

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant