Repository navigation
fix: KupiaSec review: counterpart parity, receiver home origin, LayerZero peer - #64
Conversation
There was a problem hiding this comment.
🔵 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):
ReceiverBasegains a write-oncehomeOriginand_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/resolveCounterpartnow share_recordCounterpart, which refuses a non-parity address on aPredeterminedchain (CounterpartNotParity). - #62 (LayerZero peer):
LzTransceiver.setPeerrefuses 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).
cc40896 to
6b26071
Compare
Fixes from a review by Nordin Sabli (KupiaSec) of four areas: origin authentication in
TransceiverBaseandReceiverBasefor each provider (including the vendored LayerZero OApp peer check), the permissionless WormholeexecuteVAAv1path, 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
mainonce #59 merges.Fixed
Predeterminedchain's default counterpart could be repointed after it was in use.setCounterpartandresolveCounterpartrefused 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 aPredeterminedchain.test_anExplicitCounterpartOverridesTheDefaultencoded the old behavior as an escape hatch. It is replaced: a chain where parity does not hold is gradedUnique, and through a non-parity counterpart every inbound bootstrap revertsParityBrokenanyway. Twelve fixtures that paired aPredeterminedgrade with an arbitrary counterpart now use the parity address or aUniquechain.ReceiverBasenow recordshomeOriginat 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 forop-stack-l1-l2, the eid for LayerZero, and the chainKey over an ERC-7786 gateway.ProviderReceiveSpec.test_anUnconfiguredOriginIsRejectednow delivers the real transmitter from another origin. It failed on CCIP, Hyperlane, Wormhole, and interop before this change. The newtest_aCreatedReceiverAcceptsItsTransmitterOnlyFromItsHomechecks end to end, on every binding, that the origin the transceiver writes is the one deliveries report.setPeernow refuses a peer other than the counterpart on the chain the eid maps to. Deploy step 5 indeploy/README.mdnow sets the peer after the counterpart.Not a bug
_reportingis set. A regression test with a mock mailbox that prices the refund address pins it.eip155envelope 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 theAccountAddressMismatchguard; write-once provider ids and the reverse index; bootstrap fee and float flows throughTreasury; chainKey canonicity inErc7930.parseStrictand_requireNames; the Blake2b IV, counters, and final-block flag; the Sha256 and Keccak plugins againstCommitment'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
deploy/CHECKS.md§4,DeliveryGas, andtodo.mdare updated. No payload size changes.LzTronTransceiver153 bytes,LzZkSyncTransceiver211,LzTransceiver549.Checks
forge test: 796 passed, 1 skipped (the fork test).forge lint -D notesandFOUNDRY_PROFILE=lint-src forge lint -D notes srcare clean.forge build --sizes srcis under EIP-170.🤖 Generated with Claude Code