feat: add Hyperlane verification rail (Base, Arbitrum, Stable) - #65
feat: add Hyperlane verification rail (Base, Arbitrum, Stable)#65reednaa wants to merge 2 commits into
Conversation
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>
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
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. Comment |
|
🚀 Preview deployed! Worker: |
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>
|
🚀 Preview deployed! Worker: |
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 delivershandle()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.oraclerelative to Polymer:order.inputOracleoutput.oracle@lifi/intentalready selects correctly for non-polymerverifiers (buildMandateOutputsfalls through togetOracle(verifier, token.chainId)), so no package bump is required for oracle selection.allowedOutputOracleskeeps 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:
FILL_MAGIC(0xd1252dff) that the deployed settlers require. EveryisProven()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.tsandReceiveMessage.svelte. A new local encodersrc/lib/libraries/fillPayload.tsowns the wire format, pinned by golden vectors generated from the Solidity implementation. Upstream@lifi/intent@0.2.1is still wrong; a separate PR fixes it there (lifinance/intent.ts), and this local encoder is what unblocks us meanwhile.<select>had nobind:value, sostore.verifierwas permanently"polymer"and the control was inert. Masked because the only other option wasdisabled.if (allValidated), making the refund case unreachable — a refund is precisely the case where validation did not land in time.ClaimedandRefundedwere both reported as "finalised", inflowProgress.tsandFlowStepTracker.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).
allowedOutputOraclesonly required the input chain to have a Polymer oracle — because for Polymeroutput.oracleis 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_submissionstable), which fixes three things that a first pass got wrong: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 whichfinalisereverts. This is informational only — no blocking policy was added.Scope
Hyperlane is deliberately gated to the escrow / local-demo issuance path.
lifi-order-servicehas 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.tests/e2e/escrow-hyperlane-blackbox.spec.ts(keyless, no transactions) passes, and is mutation-tested — swapping the two pinned oracle addresses fails it.orderStatus = 2(Claimed) with relay latency ~64–100s. Interchain gas ≈3.79e13 wei Arb→Base and ≈6.82e13 wei Base→Arb.tests/e2e/escrow-hyperlane-live.spec.tspassed; order0x9758237c…bedcconfirmed Claimed on Base.Reviewer notes
E2E_PRIVATE_KEYandE2E_RUN_LIVE_HYPERLANE=1) because a barenpm testsweeps the whole e2e directory and would spend real funds. Do not pass--retries— a retry after a successful submit re-pays interchain gas.quoteGasPayment, never the IGP directly: becausehook()is zero the Mailbox appliesdefaultHookplusrequiredHook, and the bare IGP quote under-reports by ~2.4x, which would revertdispatch.gasLimitis80_000 + 40_000 * numPayloads(120k for a single output), from measuredhandlecost of ~28k fixed + ~27k per payload. Hyperlane's 50,000 default does not cover even one payload.USDT0symbol as a real 6-decimal ERC-20 on the chain — two assets, 10^12 apart. Both config sites carry a comment. Useusdt0for output-side Stable demos, sincesupportsNativeOutput()is Tron-only (a pre-existing limitation that also affects native ETH elsewhere).🤖 Generated with Claude Code