Skip to content

fix: KupiaSec review: counterpart parity, receiver home origin, LayerZero peer - #64

Merged
pegahcarter merged 4 commits into
mainfrom
fix/kupiasec-review
Oct 13, 2026
Merged

pegahcarter merged 4 commits into
mainfrom
fix/kupiasec-review

Conversation

@pegahcarter

Copy link
Copy Markdown
Contributor

Fixes from a review by Nordin Sabli (KupiaSec) of four areas: origin authentication in TransceiverBase and ReceiverBase for each provider (including the vendored LayerZero OApp peer check), the permissionless Wormhole executeVAAv1 path, bootstrap and receiver reports, and Commitment and ERC-7930 encoding.

Stacked on #59, because the Wormhole Executor path under review lands there. Retarget this PR to main once #59 merges.

Fixed

  • Transceiver: a Predetermined chain's default counterpart can be repointed after it is in use #60 (Medium): a Predetermined chain's default counterpart could be repointed after it was in use. setCounterpart and resolveCounterpart refused only when a counterpart was stored, but the parity default is live from the moment a chain is graded. Both now go through _recordCounterpart, which refuses an address other than _parityAddress(chainKey) on a Predetermined chain. test_anExplicitCounterpartOverridesTheDefault encoded the old behavior as an escape hatch. It is replaced: a chain where parity does not hold is graded Unique, and through a non-parity counterpart every inbound bootstrap reverts ParityBroken anyway. Twelve fixtures that paired a Predetermined grade with an arbitrary counterpart now use the parity address or a Unique chain.
  • Receivers on CCIP, Hyperlane, Wormhole, and Superchain interop accept their transmitter's address from any origin chain #61 (Medium): CCIP, Hyperlane, Wormhole, Superchain interop, and ERC-7786 receivers accepted their transmitter's address from any origin. ReceiverBase now records homeOrigin at initialization, and _onMessageFrom(origin, sender, payload) refuses any other origin. The origin is in the transport's own naming: the provider id from the transceiver's table (ProviderTransceiver._accountInitializer), the source chain id for interop, the messenger for op-stack-l1-l2, the eid for LayerZero, and the chainKey over an ERC-7786 gateway. ProviderReceiveSpec.test_anUnconfiguredOriginIsRejected now delivers the real transmitter from another origin. It failed on CCIP, Hyperlane, Wormhole, and interop before this change. The new test_aCreatedReceiverAcceptsItsTransmitterOnlyFromItsHome checks end to end, on every binding, that the origin the transceiver writes is the one deliveries report.
  • LayerZero transceiver: a peer can disagree with the counterpart, and both are write-once #62 (Low): the LayerZero transceiver's peer could disagree with its counterpart, and both are write-once. setPeer now refuses a peer other than the counterpart on the chain the eid maps to. Deploy step 5 in deploy/README.md now sets the peer after the counterpart.

Not a bug

  • Receiver report: the quote and the send run under different refund addresses #63 (closed): I misread the order of the report's quote and send. The quote is evaluated after _reporting is set. A regression test with a mock mailbox that prices the refund address pins it.
  • An eip155 envelope accepts addresses of any length. The CAIP-350 eip155 profile says addresses are 20 bytes today but may become 32, so a strict rule would be a policy choice. No issue was filed.

Reviewed, no findings

The Wormhole consumed-hash set (keyed on vm.hash, per contract, consumed only after the (targetChain, targetAddress) prefix matches this contract on this chain); zkSync and Tron CREATE2 derivation and the AccountAddressMismatch guard; write-once provider ids and the reverse index; bootstrap fee and float flows through Treasury; chainKey canonicity in Erc7930.parseStrict and _requireNames; the Blake2b IV, counters, and final-block flag; the Sha256 and Keccak plugins against Commitment's fold; cross-chain replay of a commitment (seeded with the destination chainKey) and of a path A payload (each transport binds the destination).

Cost

Checks

forge test: 796 passed, 1 skipped (the fork test). forge lint -D notes and FOUNDRY_PROFILE=lint-src forge lint -D notes src are clean. forge build --sizes src is under EIP-170.

🤖 Generated with Claude Code

Copilot AI balanced review requested due to automatic review settings October 13, 2026 16:35

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 rewrites origin-authentication across six provider bindings and alters write-once counterpart/peer invariants and the receiver storage layout, which is security-critical and warrants final human review despite no defects being found.

0 open findings

What changed in this PR

This PR applies fixes from a KupiaSec (Nordin Sabli) security review of the EVM messaging layer, hardening origin authentication and write-once configuration. It is central to the protocol's cross-chain trust model: it closes paths by which a compromised transceiver owner or a provider's deploy key could repoint a live lane or forge another account's payloads.

Changes:

  • #61 (receiver home origin): ReceiverBase gains a write-once homeOrigin and _onMessageFrom(origin, sender, payload) that rejects any origin but the account's home. Each native binding now reports the origin in its transport's own naming (CCIP selector, Hyperlane domain, Wormhole emitter chain, interop source chain id, LZ eid, op-stack messenger), and each transceiver arms the receiver with the matching value.
  • #60 (counterpart parity): setCounterpart/resolveCounterpart now share _recordCounterpart, which refuses a non-parity address on a Predetermined chain (CounterpartNotParity).
  • #62 (LayerZero peer): LzTransceiver.setPeer refuses any peer other than the counterpart on the eid's chain; deploy order updated so the peer is set after the counterpart.
File Description
src/​messaging/​inbound/​ReceiverBase.sol Adds homeOrigin, NotHomeOrigin/ZeroHomeOrigin; folds origin check into _onMessageFrom; new initializer arg
src/​messaging/​transceiver/​TransceiverBase.sol New _recordCounterpart with parity guard; arms receiver with homeChainKey
src/​protocols/​ProviderTransceiver.sol Overrides _accountInitializer to arm a receiver with the home's provider id
src/​protocols/​{ccip,hyperlane}/​*DivergentTransceiver.sol Diamond-resolving _accountInitializer overrides
src/​protocols/​op-stack/​OpStackInteropTransceiver.sol, OpStackInteropReceiver.sol, OpStackReceiver.sol Arm/report origin by source chain id / messenger
src/​protocols/​{ccip,hyperlane,wormhole}/​*Receiver.sol Report origin alongside sender in delivery callbacks
src/​protocols/​layerzero/​{LzTransceiver,LzReceiver}.sol setPeer counterpart check; receiver armed/report by eid
src/​messaging/​DeliveryGas.sol, docs/​*, deploy/​{README,CHECKS}.md Updated gas figures, procedure, and spec/research/todo prose
test/​** Updated fixtures/initializers; new tests for parity, home-origin, peer-counterpart, and the #63 refund-address regression

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

…rity address

The parity default is in use as soon as the chain is graded, so recording any
other address later repointed a live lane. Fixtures that paired a Predetermined
grade with a non-parity counterpart, which production can no longer reach, now
use the parity address or a Unique chain.

Fixes #60. Reported by Nordin Sabli (KupiaSec).
CCIP, Hyperlane, Wormhole, interop, and ERC-7786 receivers checked the sender's
address but not its chain. Where the provider's transceiver is not yet
deployed, its deployedBy can place code at any transmitter's address and send
from there. A receiver now records its home in the transport's own naming at
initialization (the provider id, the interop chain id, or the chainKey) and
refuses any other origin.

Cost: one storage slot per receiver, measured at 22k to 26k gas per bootstrap
and 1.3k to 2.3k per payload; no payload size change.

Fixes #61. Reported by Nordin Sabli (KupiaSec).
…rpart

LayerZero sends to and admits only the peer while the base authenticates the
counterpart, and both are write-once, so a mismatch closed the lane for good.
The send suite's registry-less harness now gets a registry recording its
counterpart, since a peer can no longer be set without one.

Fixes #62. Reported by Nordin Sabli (KupiaSec).
…ddress

The mock mailbox can now price the metadata's refund address, so a quote taken
over different metadata than the send would underpay and revert. The report
already quotes inside `_reporting`; this guards that order (#63, not a bug).
@pegahcarter
pegahcarter changed the base branch from feat/wormhole-executor to main October 13, 2026 18:10
@pegahcarter
pegahcarter merged commit 259e668 into main Oct 13, 2026
1 check passed
@pegahcarter
pegahcarter deleted the fix/kupiasec-review branch October 13, 2026 18:11
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.

2 participants