Skip to content

feat(wormhole): deliver through the Executor with a provider's signed quote - #59

Merged
pegahcarter merged 9 commits into
mainfrom
feat/wormhole-executor
Oct 13, 2026
Merged

pegahcarter merged 9 commits into
mainfrom
feat/wormhole-executor

Conversation

@pegahcarter

@pegahcarter pegahcarter commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #53. The Wormhole binding quoted and sent through ExecutorQuoterRouter, the Executor's optional on-chain quote path, which is deployed only on Polygon and Monad. Wormhole's production path is the Executor with a relay provider's signed quote, fetched off-chain (POST executor.labsapis.com/v0/quote). The Standard Relayer's automatic delivery ended 2026-04-01.

How a send works now

  • Each send carries one of two attributes. Without either, quote and send revert NoSignedQuote.
    • crossecute.wormhole.execution: abi.encode(signedQuote, gasLimit), with gasLimit 0 meaning the DeliveryGas default.
    • crossecute.wormhole.publishOnly, with no body, for a sender whose calldata is fixed long before it lands (a Safe), past the quote's one-hour expiry.
  • WormholeMessage checks the quote is EQ01, for this chain and the route's destination, and unexpired, then prices it on chain with the Executor's own formula (ExecutorQuoter.estimateQuote): base fee + gas × destination gas price × destination/source USD price. quote and send share one execution() so they agree.
  • send publishes, pays exactly the price to Executor.requestExecution (which forwards everything to the quote's payee and checks no payment), and refunds the rest to _refundTo(). The quote's signature is not checked on chain: a bad quote costs only the caller who chose it.
  • A publish-only send pays Core's fee and stops. Whoever holds the signed VAA (Wormholescan serves it) delivers it with executeVAAv1, paying destination gas. That path trusts no one beyond the guardians, and a relay provider adds no trust either: it can only submit the same VAA, which the receiver verifies.

History in this PR. Commits 8598011 and cf96789 added deferred execution through a relay provider (escrow, approved quoters, on-chain quote-signature check). They are reverted in f0dd009 and ad3b01f in favour of publish-only, which needs none of that machinery.

Verified against live infrastructure (no gas)

  • The four configured Executors answer ourChain() 2/30/23/24 and Executor-0.0.1.
  • A live Ethereum→Base quote for 1,000,000 gas, decoded and priced by the binding, equals the API's estimatedCost (84,081,300,000,000 wei) to the wei: test_aLiveQuoteIsPricedAsTheExecutorApiPricesIt.
  • Mutations of the formula, the refund, the publish-only branch, and its empty-body check each fail the suite.

Also

  • Wormhole has no chain id for zkSync Era or Tron, so WormholeZkSyncTransceiver / WormholeTronTransceiver are removed (decided), and WormholeTransceiverBase is folded into WormholeTransceiver.
  • Deploy inputs: WORMHOLE_EXECUTOR replaces WORMHOLE_EXECUTOR_ROUTER and WORMHOLE_QUOTER.
  • Shared specs take the attributes every send needs from a new fixture hook, _attributes() (empty for the other five; Wormhole's quote), and the send harness's quote takes attributes.
  • Docs: spec P9, R1.5, §6 step 2 (also now names LayerZero's DVN pin); deploy/CHECKS.md (Executor addresses, quote API, VAA API, lane row); provider-research §6; README; todo (a job that delivers publish-only VAAs).
  • WormholeTransceiver is 23,268 bytes (1,308 under EIP-170).

791 tests pass, 1 skipped (the LayerZero fork test); lint clean on both profiles.

🤖 Generated with Claude Code

… quote

The binding quoted and sent through ExecutorQuoterRouter, which is deployed
only on Polygon and Monad. Each send now carries a relay provider's signed
EQ01 quote in its one attribute; the binding checks its chains and expiry,
prices it on chain with the Executor's formula (matching the Executor API's
estimatedCost on a live quote), pays that to Executor.requestExecution, and
refunds the rest, since the Executor forwards everything to the payee. A
send without a quote reverts.

Wormhole reaches neither zkSync Era nor Tron, so its divergent transceivers
are removed. Shared specs take the attributes every send needs from a new
fixture hook.

Fixes #53
Copilot AI balanced review requested due to automatic review settings October 8, 2026 05:21

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It reworks on-chain payment/pricing logic for a deployed cross-chain contract and introduces a quote/send consistency gap (recipient no longer validated in quote), so it warrants human review.

2 open findings
What changed in this PR

This PR rewrites the Wormhole binding's outbound path. Previously it quoted and paid through ExecutorQuoterRouter, which issue #53 showed is deployed only on Polygon and Monad (not on any configured chain). The binding now carries a relay provider's off-chain signed EQ01 quote as a required attribute (crossecute.wormhole.execution), prices it on-chain with the Executor's own formula, publishes via Core, pays the computed price to Executor.requestExecution, and refunds the remainder itself. Because Wormhole reaches no zkSync Era or Tron chain, the divergent transceiver variants are deleted and the base is folded into WormholeTransceiver.

Changes:

  • Replace the on-chain quoter router with a caller-supplied signed quote; add on-chain pricing (executionPrice), quote validation (chains/expiry/prefix), and a self-managed refund.
  • Remove WormholeZkSyncTransceiver/WormholeTronTransceiver and WormholeTransceiverBase; collapse deploy inputs to WORMHOLE_CORE + WORMHOLE_EXECUTOR.
  • Add a shared _attributes() fixture hook so every send carries its required attributes; add MockExecutor, update specs and docs (spec/research/todo/README/CHECKS).
File Description
src/​protocols/​wormhole/​WormholeMessage.sol New signed-quote send/quote, EQ01 parsing + pricing, refund
src/​protocols/​wormhole/​WormholeTransceiver.sol Base folded in; executor immutable replaces router/quoter
src/​protocols/​wormhole/​WormholeTransmitter.sol Same endpoint/signature update
src/​protocols/​wormhole/​WormholeDivergentTransceiver.sol Deleted (no zkSync/Tron)
script/​DeployWormhole.s.sol, script/​deploy/​WormholeDeploy.sol Deploy inputs reduced to core+executor; zkSync deploy removed
test/​protocols/​wormhole/​* MockExecutor replaces MockExecutorQuoterRouter; fixtures/specs updated; live-quote test added
test/​protocols/​ProviderFixture.sol, ProviderBindingSpec.t.sol _attributes() hook; quoteMessagePublic takes attributes
test/​protocols/​{ccip,hyperlane,layerzero,op-stack}/​* Implement _attributes() (empty) and new quote signature
test/​deploy/​ProductionDeploy.t.sol Wormhole deploy env updated
docs/​*, deploy/​CHECKS.md, README.md Document the signed-quote path, Executor addresses, lane row

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread contracts/evm/src/protocols/wormhole/WormholeMessage.sol
Comment thread contracts/evm/test/protocols/op-stack/OpStackInteropFixture.sol Outdated
…e send

The signed-quote rewrite dropped the recipient from WormholeMessage.quote, so a
quote priced a message the send refuses (R2.5). ProviderEvmRecipientSpec now
checks the quote too, for every binding that runs it.
A Safe signs calldata long before it executes, so it cannot carry a signed
quote that expires within the hour. A send with the deferred attribute
publishes and escrows a budget; anyone later calls requestDeferredExecution
with a fresh quote. The escrow pays only a quote whose signature recovers to
its own quoter field, a quoter the transceiver's owner approves, within the
budget. The transceiver's withdraw leaves escrow in place, and a send may not
spend it. Adds WORMHOLE_QUOTER to the deploy inputs.
Reverts cf96789. Deferral is replaced by publish-only, delivered by anyone
through executeVAAv1; the docs follow in the publish-only commit.
Reverts 8598011. The relay provider adds no trust a hand delivery lacks: it can
only submit the guardian-signed VAA, which executeVAAv1 verifies. The escrow,
quoter set, and signature check guarded only its fee, so publish-only replaces
them with one branch.
A multisig's calldata is fixed long before it lands, past the hour a relay
provider's quote lasts. With the publish-only attribute a send pays Core alone
and requests no execution; whoever holds the signed VAA delivers it through
executeVAAv1, which already verifies it, so no party is trusted beyond the
guardians.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It reworks a security-sensitive cross-chain payment path with hand-rolled byte parsing of off-chain signed financial quotes and a new on-chain pricing formula verified against a single live data point, which warrants human review despite the comprehensive tests.

0 open findings

2 resolved since last review

🧠 Review effort: Balanced

@pegahcarter
pegahcarter merged commit b03af7d into main Oct 13, 2026
2 checks passed
@pegahcarter
pegahcarter deleted the feat/wormhole-executor branch October 13, 2026 18:10
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.

Wormhole: no ExecutorQuoterRouter on any configured chain

2 participants