Repository navigation
feat(wormhole): deliver through the Executor with a provider's signed quote - #59
Conversation
… 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
There was a problem hiding this comment.
🔵 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/WormholeTronTransceiverandWormholeTransceiverBase; collapse deploy inputs toWORMHOLE_CORE+WORMHOLE_EXECUTOR. - Add a shared
_attributes()fixture hook so every send carries its required attributes; addMockExecutor, 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.
…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.
There was a problem hiding this comment.
🔵 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


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
NoSignedQuote.crossecute.wormhole.execution:abi.encode(signedQuote, gasLimit), withgasLimit0 meaning theDeliveryGasdefault.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.WormholeMessagechecks 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.quoteandsendshare oneexecution()so they agree.sendpublishes, pays exactly the price toExecutor.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.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)
ourChain()2/30/23/24 andExecutor-0.0.1.estimatedCost(84,081,300,000,000 wei) to the wei:test_aLiveQuoteIsPricedAsTheExecutorApiPricesIt.Also
WormholeZkSyncTransceiver/WormholeTronTransceiverare removed (decided), andWormholeTransceiverBaseis folded intoWormholeTransceiver.WORMHOLE_EXECUTORreplacesWORMHOLE_EXECUTOR_ROUTERandWORMHOLE_QUOTER._attributes()(empty for the other five; Wormhole's quote), and the send harness's quote takes attributes.WormholeTransceiveris 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