From cf329ef123575142f0be29284e0db2be72465a98 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 10 Sep 2026 15:41:09 -0600 Subject: [PATCH 1/2] Document ERC7786OpenBridge trust boundaries and delivery semantics Consolidates NatSpec follow-ups from a series of external reports on `ERC7786OpenBridge` whose disposition depended on already-in-code but undocumented design intent: - `sendMessage`: aligns with `AxelarGatewayAdapter.sendMessage` on the source-side non-validation of the recipient's address component, and states explicitly that the per-gateway `try`/`catch` isolates gateway reverts (not interface non-compliance from gateways returning EVM-successful but malformed data). - `receiveMessage`: documents that the public retry path only performs work once the threshold has been reached from gateway deliveries, and frames the wrong-return-value revert as a deliberate signal treating the recipient as the interface-violating party rather than the bridge. - `_addGateway`: states that registering a gateway trusts it to implement the ERC-7786 interface, and that the `code.length > 0` check is a fat-finger guard against plain EOAs (not an interface attestation, and in particular passing for EIP-7702 delegated EOAs). Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/crosschain/ERC7786OpenBridge.sol | 34 ++++++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/contracts/crosschain/ERC7786OpenBridge.sol b/contracts/crosschain/ERC7786OpenBridge.sol index 09dd2345..37565b29 100644 --- a/contracts/crosschain/ERC7786OpenBridge.sol +++ b/contracts/crosschain/ERC7786OpenBridge.sol @@ -94,8 +94,23 @@ contract ERC7786OpenBridge is IERC7786GatewaySource, IERC7786Recipient, Ownable, return false; } - /// @inheritdoc IERC7786GatewaySource - /// @dev Using memory instead of calldata avoids stack too deep errors + /** + * @inheritdoc IERC7786GatewaySource + * + * @dev Using memory instead of calldata avoids stack too deep errors. + * + * NOTE: This function does not enforce a maximum size for `payload` nor validate the address component of a + * validly encoded `recipient`; the caller is responsible for supplying values that the destination chain can + * decode and that resolve to a live recipient. Oversized payloads or recipients with invalid or non-contract + * targets will surface as undeliverable messages on the destination side. + * + * NOTE: The per-gateway {try}/{catch} isolates gateway reverts, allowing the aggregate send to proceed as long + * as the {getThreshold} number of gateways succeed. It does not, and is not intended to, isolate ABI-decoding + * failures produced in this function's own frame when a gateway returns EVM success with malformed data (fewer + * than 32 bytes, oversized returndata, or an unexpected shape). Registered gateways are trusted to implement + * the {IERC7786GatewaySource} interface; interface non-compliance is deliberately propagated so a non-conforming + * gateway surfaces loudly rather than being silently counted as a successful send. + */ function sendMessage( bytes calldata recipient, // Binary Interoperable Address bytes calldata payload, @@ -156,6 +171,9 @@ contract ERC7786OpenBridge is IERC7786GatewaySource, IERC7786Recipient, Ownable, * * It can also be called by anyone (including an ERC-7786 gateway) to retry the execution. This can be useful if * the automatic execution (that is triggered when the threshold is reached) fails, and someone wants to retry it. + * The retry path only performs work when {getThreshold} gateway deliveries have already been recorded for the + * message; a caller that is not a registered gateway against a message that has not yet accumulated enough + * receipts falls through the counting branch and returns the magic value without executing anything. * * When a message is forwarded by a known gateway, a {Received} event is emitted. If a known gateway calls this * function more than once (for a given message), only the first call is counts toward the threshold and emits an @@ -168,7 +186,9 @@ contract ERC7786OpenBridge is IERC7786GatewaySource, IERC7786Recipient, Ownable, * * someone tries re-execute a message that was already successfully delivered. This includes gateways that call * this function a second time with a message that was already executed. * * the execution of the message (on the {IERC7786Recipient} recipient) is successful but fails to return the - * executed value. + * expected magic value. This is deliberate: a recipient that succeeds with a non-conforming return value is + * treated as an interface violation on the recipient side, and Solidity's canonical revert is used as the + * signal. The message becomes deliverable again once the recipient is updated to return the correct value. * * This function does not revert if: * @@ -302,6 +322,14 @@ contract ERC7786OpenBridge is IERC7786GatewaySource, IERC7786Recipient, Ownable, // ================================================== Internal =================================================== + /** + * @dev Adds a gateway to the authorized set. Registering a gateway trusts it to implement the + * {IERC7786GatewaySource} interface correctly; non-conforming gateways (returning malformed data, returning an + * unexpected shape, or refusing to relay) are the operator's responsibility to identify and rotate out via + * {removeGateway}. The `gateway.code.length > 0` check is a fat-finger guard against registering a plain EOA; + * it does not attest that the address implements the interface (for example, an EIP-7702 delegated EOA carries + * a 23-byte delegation indicator and would pass this check). + */ function _addGateway(address gateway) internal virtual { require(gateway.code.length > 0, ERC7786OpenBridgeGatewayNotAContract(gateway)); require(_gateways.add(gateway), ERC7786OpenBridgeGatewayAlreadyRegistered(gateway)); From bafa0e01bb70f6ccf50d67e5edfa0d7502d2bb6a Mon Sep 17 00:00:00 2001 From: ernestognw Date: Thu, 10 Sep 2026 15:49:07 -0600 Subject: [PATCH 2/2] Trim NatSpec and move try/catch rationale inline MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Drop the "undeliverable messages" trailer and the try/catch-scope prose from `sendMessage` NatSpec (implied by the "not enforced" note and by the general trust-in-gateways statement). - Move the try/catch rationale into an inline comment at the call site, where the choice actually lives. - Drop the "This is deliberate…" justification from `receiveMessage` NatSpec and instead name `ERC7786OpenBridgeInvalidExecutionReturnValue` as the observable revert on wrong magic value. Co-Authored-By: Claude Opus 4.7 (1M context) --- contracts/crosschain/ERC7786OpenBridge.sol | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/contracts/crosschain/ERC7786OpenBridge.sol b/contracts/crosschain/ERC7786OpenBridge.sol index 37565b29..94240a06 100644 --- a/contracts/crosschain/ERC7786OpenBridge.sol +++ b/contracts/crosschain/ERC7786OpenBridge.sol @@ -101,15 +101,11 @@ contract ERC7786OpenBridge is IERC7786GatewaySource, IERC7786Recipient, Ownable, * * NOTE: This function does not enforce a maximum size for `payload` nor validate the address component of a * validly encoded `recipient`; the caller is responsible for supplying values that the destination chain can - * decode and that resolve to a live recipient. Oversized payloads or recipients with invalid or non-contract - * targets will surface as undeliverable messages on the destination side. + * decode and that resolve to a live recipient. * - * NOTE: The per-gateway {try}/{catch} isolates gateway reverts, allowing the aggregate send to proceed as long - * as the {getThreshold} number of gateways succeed. It does not, and is not intended to, isolate ABI-decoding - * failures produced in this function's own frame when a gateway returns EVM success with malformed data (fewer - * than 32 bytes, oversized returndata, or an unexpected shape). Registered gateways are trusted to implement - * the {IERC7786GatewaySource} interface; interface non-compliance is deliberately propagated so a non-conforming - * gateway surfaces loudly rather than being silently counted as a successful send. + * NOTE: Registered gateways are trusted to implement the {IERC7786GatewaySource} interface correctly. The + * aggregate send tolerates a subset of gateways failing at runtime, but relies on each gateway conforming to + * the interface it was registered under. */ function sendMessage( bytes calldata recipient, // Binary Interoperable Address @@ -136,6 +132,9 @@ contract ERC7786OpenBridge is IERC7786GatewaySource, IERC7786Recipient, Ownable, for (uint256 i = 0; i < outbox.length; ++i) { address gateway = _gateways.at(i); // send message + // + // The typed try/catch is intentional: delegates to Solidity's canonical decoding to reject a gateway + // that returns EVM success with a value that does not conform to the interface it was registered under. try IERC7786GatewaySource(gateway).sendMessage(bridge, wrappedPayload, attributes) returns ( bytes32 id ) { @@ -186,9 +185,7 @@ contract ERC7786OpenBridge is IERC7786GatewaySource, IERC7786Recipient, Ownable, * * someone tries re-execute a message that was already successfully delivered. This includes gateways that call * this function a second time with a message that was already executed. * * the execution of the message (on the {IERC7786Recipient} recipient) is successful but fails to return the - * expected magic value. This is deliberate: a recipient that succeeds with a non-conforming return value is - * treated as an interface violation on the recipient side, and Solidity's canonical revert is used as the - * signal. The message becomes deliverable again once the recipient is updated to return the correct value. + * expected magic value (reverts with {ERC7786OpenBridgeInvalidExecutionReturnValue}). * * This function does not revert if: *