Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions documentation/lz-bridge/GATEWAY_UPGRADE_NOTES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Gateway Upgrade Notes

## `isReceiveEnabled`

The `LZBridgeGateway.isReceiveEnabled` flag is provided for use during future gateway replacements, so that a disabled old gateway can continue delivering in-flight LZ messages instead of reverting them.

- Managed automatically by `enable()` (sets `true`) and `disable()` (sets `false`).
- Can be set manually via `setIsReceiveEnabled()`, gated to `emergency` / `admin` roles.

### Expected usage

1. **OCG proposal** calls `oldGateway.disable("")` then `oldGateway.setIsReceiveEnabled(true)` (and enables the new gateway). The old gateway can no longer send but still delivers incoming messages.
2. **DAO MS** reconfigures non-canonical chains at its own pace; in-flight messages continue to arrive at the old gateway.
3. **Old gateway is deactivated in the Kernel** once operations are complete. Calling `setIsReceiveEnabled(false)` is not required — Kernel deactivation is sufficient.
39 changes: 37 additions & 2 deletions src/policies/bridge/LZBridgeGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ contract LZBridgeGateway is
/// @inheritdoc ILZBridgeGateway
uint256 public override bridgedSupply;

/// @inheritdoc ILZBridgeGateway
bool public override isReceiveEnabled;

/// @inheritdoc ILZBridgeGateway
mapping(uint32 eid_ => bytes32) public override peers;

Expand Down Expand Up @@ -174,6 +177,18 @@ contract LZBridgeGateway is
return (1, 0);
}

// ========= POLICY ENABLER ========= //

/// @dev Sets isReceiveEnabled to true when the gateway is enabled.
function _enable(bytes calldata) internal override {
if (!isReceiveEnabled) _setIsReceiveEnabled(true);
}

/// @dev Resets isReceiveEnabled to false when the gateway is disabled.
function _disable(bytes calldata) internal override {
if (isReceiveEnabled) _setIsReceiveEnabled(false);
}

// ========= OHM BRIDGING ========= //

/// @inheritdoc ILZBridgeGateway
Expand Down Expand Up @@ -263,7 +278,7 @@ contract LZBridgeGateway is

/// @inheritdoc ILayerZeroReceiver
/// @dev Reverts if:
/// - The gateway is not enabled.
/// - Receiving is not enabled (isReceiveEnabled is false).
/// - The caller is not the LayerZero endpoint.
/// - The origin sender is not the configured peer for the source endpoint ID.
/// - No peer is configured for the source endpoint ID.
Expand All @@ -277,7 +292,8 @@ contract LZBridgeGateway is
bytes calldata message_,
address,
bytes calldata
) external payable override onlyEnabled {
) external payable override {
if (!isReceiveEnabled) revert LZBridgeGateway_ReceiveNotEnabled();
if (msg.sender != LZ_ENDPOINT) revert LZBridgeGateway_OnlyEndpoint();
if (_getPeerOrRevert(origin_.srcEid) != origin_.sender)
revert LZBridgeGateway_OnlyPeer(origin_.srcEid, origin_.sender);
Expand Down Expand Up @@ -305,6 +321,20 @@ contract LZBridgeGateway is
emit PeerSet(eid_, peer_);
}

/// @inheritdoc ILZBridgeGateway
/// @dev Reverts if:
/// - The caller does not have the emergency or admin role.
/// - The gateway is currently enabled.
/// - The value is already in the desired state.
function setIsReceiveEnabled(
bool isReceiveEnabled_
) external override onlyEmergencyOrAdminRole {
if (isEnabled) revert LZBridgeGateway_ReceiveControlOnlyWhenDisabled();
if (isReceiveEnabled == isReceiveEnabled_)
revert LZBridgeGateway_ReceiveAlreadyInDesiredState();
_setIsReceiveEnabled(isReceiveEnabled_);
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/// @inheritdoc ILZBridgeGateway
/// @dev Reverts if:
/// - The caller does not have the bridge_admin or admin role.
Expand Down Expand Up @@ -534,6 +564,11 @@ contract LZBridgeGateway is

// ========= PRIVATE FUNCTIONS ========= //

function _setIsReceiveEnabled(bool isReceiveEnabled_) private {
isReceiveEnabled = isReceiveEnabled_;
emit IsReceiveEnabledSet(isReceiveEnabled_);
}

/// @notice Decodes the message type from the payload and routes to the appropriate handler.
function _decodeAndRoute(uint32 srcEid_, bytes32 guid_, bytes calldata payload_) private {
if (payload_.length < _MIN_PAYLOAD_LENGTH) revert LZBridgeGateway_InvalidPayload();
Expand Down
28 changes: 28 additions & 0 deletions src/policies/interfaces/ILZBridgeGateway.sol
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ interface ILZBridgeGateway is IVersioned, ILZEndpointV2Admin {
/// @param options The invalid options bytes.
error LZBridgeGateway_InvalidOptions(bytes options);

/// @notice Thrown when lzReceive is called while receiving is disabled.
error LZBridgeGateway_ReceiveNotEnabled();

/// @notice Thrown when setIsReceiveEnabled is called while the gateway is enabled.
error LZBridgeGateway_ReceiveControlOnlyWhenDisabled();

/// @notice Thrown when setIsReceiveEnabled is called with the current value.
error LZBridgeGateway_ReceiveAlreadyInDesiredState();

// ========= EVENTS ========= //

/// @notice Emitted when OHM is burned and sent to another chain.
Expand Down Expand Up @@ -94,6 +103,10 @@ interface ILZBridgeGateway is IVersioned, ILZEndpointV2Admin {
/// @param enforcedOptions The enforced option parameters.
event EnforcedOptionsSet(EnforcedOptionParam[] enforcedOptions);

/// @notice Emitted when the isReceiveEnabled flag is changed.
/// @param isReceiveEnabled The new value.
event IsReceiveEnabledSet(bool isReceiveEnabled);

// ========= CORE FUNCTIONS ========= //

/// @notice Burns OHM held by the gateway and sends a bridge message to a destination chain.
Expand Down Expand Up @@ -141,6 +154,15 @@ interface ILZBridgeGateway is IVersioned, ILZEndpointV2Admin {
/// @param peer_ The peer (remote gateway) address (bytes32 or `bytes32(0)` to clear).
function setPeer(uint32 eid_, bytes32 peer_) external;

/// @notice Sets whether the gateway can receive messages.
/// @dev Only callable by the emergency or admin role.
/// Managed automatically by enable()/disable(), but can be set manually
/// to allow receiving while the gateway is disabled
/// (e.g. during gateway replacements, to deliver in-flight messages).
///
/// @param isReceiveEnabled_ The desired state.
function setIsReceiveEnabled(bool isReceiveEnabled_) external;

/// @notice Sets the delegate on the LayerZero endpoint.
/// @dev Only callable by the bridge_admin or admin role.
///
Expand Down Expand Up @@ -220,6 +242,12 @@ interface ILZBridgeGateway is IVersioned, ILZEndpointV2Admin {
bytes calldata extraOptions_
) external view returns (bytes memory);

/// @notice Whether lzReceive() can process incoming messages.
/// @dev Automatically set to true by enable() and false by disable().
/// Can be manually set via setIsReceiveEnabled() to allow receiving
/// while the gateway is otherwise disabled (e.g., during gateway replacements).
function isReceiveEnabled() external view returns (bool);

/// @notice Returns whether this is the canonical (mainnet) chain.
// solhint-disable-next-line func-name-mixedcase
function IS_CANONICAL() external view returns (bool);
Expand Down
3 changes: 0 additions & 3 deletions src/proposals/LZBridgeSecurityUpgradeProposal.sol
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,10 @@ import {ProposalScript} from "src/proposals/ProposalScript.sol";
import {LZConfigLib} from "src/libraries/LZConfigLib.sol";

// Interfaces
import {EnforcedOptionParam} from "@lz-oapp-evm-0.4.1/oapp/interfaces/IOAppOptionsType3.sol";
import {ExecutorConfig} from "@lz-evm-messagelib-v2-3.0.162/SendLibBase.sol";
import {UlnConfig} from "@lz-evm-messagelib-v2-3.0.162/uln/UlnBase.sol";
import {ILayerZeroEndpointV2} from "@lz-evm-protocol-v2-3.0.162/interfaces/ILayerZeroEndpointV2.sol";
import {IEndpointV2State} from "src/interfaces/layerzero/IEndpointV2State.sol";
import {SetConfigParam} from "@lz-evm-protocol-v2-3.0.162/interfaces/IMessageLibManager.sol";

// Constants
import {ADMIN_ROLE} from "src/policies/utils/RoleDefinitions.sol";
Expand All @@ -31,7 +29,6 @@ import {RolesAdmin} from "src/policies/RolesAdmin.sol";
import {ROLESv1} from "src/modules/ROLES/ROLES.v1.sol";
import {LZBridgeGateway} from "src/policies/bridge/LZBridgeGateway.sol";
import {LZBridgeActivator} from "src/proposals/LZBridgeActivator.sol";
import {ILZBridgeGateway} from "src/policies/interfaces/ILZBridgeGateway.sol";
import {PolicyEnabler} from "src/policies/utils/PolicyEnabler.sol";

/// @notice OCG proposal for the LayerZero Bridge Security Upgrade.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,26 @@ contract LZBridgeGatewayTests_BurnAndSend is LZBridgeGatewayTestBase {
vm.stopPrank();
}

function test_burnAndSend_revertsIfBridgeDisabledAndReceiveEnabled() external {
vm.startPrank(admin);
gateway.disable(bytes(""));
gateway.setIsReceiveEnabled(true);
vm.stopPrank();

vm.startPrank(facilitator);
ohm.transfer(address(gateway), 100e9);

vm.expectRevert(abi.encodeWithSelector(IEnabler.NotEnabled.selector));
gateway.burnAndSend{value: 1 ether}(
NONCANONICAL_EID,
recipient,
100e9,
payable(facilitator),
bytes("")
);
vm.stopPrank();
}

function testFuzz_burnAndSend_revertsIfNotBridgeFacilitator(address caller_) external {
vm.assume(caller_ != facilitator);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
// SPDX-License-Identifier: AGPL-3.0
pragma solidity >=0.8.30;

import {Vm} from "forge-std/Vm.sol";

import {LZBridgeGatewayTestBase} from "src/test/policies/bridge/LZBridgeGateway/LZBridgeGatewayTestBase.sol";

// Interfaces
import {IEnabler} from "src/periphery/interfaces/IEnabler.sol";
import {ILZBridgeGateway} from "src/policies/interfaces/ILZBridgeGateway.sol";
import {IPolicyAdmin} from "src/policies/interfaces/utils/IPolicyAdmin.sol";

// Constants
Expand All @@ -25,6 +28,35 @@ contract LZBridgeGatewayTests_EnableDisable is LZBridgeGatewayTestBase {
vm.stopPrank();
}

function test_enable_setsIsReceiveEnabledTrue() external {
vm.startPrank(admin);
gateway.disable(bytes(""));
assertFalse(gateway.isReceiveEnabled(), "isReceiveEnabled should be false after disable");

gateway.enable(bytes(""));
assertTrue(gateway.isReceiveEnabled(), "enable should set isReceiveEnabled to true");
vm.stopPrank();
}

function test_enable_skipsIsReceiveEnabledEventIfAlreadyTrue() external {
vm.startPrank(admin);
gateway.disable(bytes(""));
gateway.setIsReceiveEnabled(true);
assertTrue(gateway.isReceiveEnabled(), "isReceiveEnabled should be true after manual set");

// enable() should NOT emit IsReceiveEnabledSet because it is already true
vm.recordLogs();
gateway.enable(bytes(""));

Vm.Log[] memory logs = vm.getRecordedLogs();
bytes32 eventSig = ILZBridgeGateway.IsReceiveEnabledSet.selector;
for (uint256 i = 0; i < logs.length; ++i) {
assertTrue(logs[i].topics[0] != eventSig, "Should not emit IsReceiveEnabledSet");
}
assertTrue(gateway.isReceiveEnabled(), "isReceiveEnabled should remain true after enable");
vm.stopPrank();
}

function test_enable_revertsIfAlreadyEnabled() external {
vm.expectRevert(abi.encodeWithSelector(IEnabler.NotDisabled.selector));
vm.prank(admin);
Expand All @@ -49,6 +81,24 @@ contract LZBridgeGatewayTests_EnableDisable is LZBridgeGatewayTestBase {
assertFalse(gateway.isEnabled(), "Should be disabled");
}

function test_disable_setsIsReceiveEnabledFalse() external {
assertTrue(gateway.isReceiveEnabled(), "isReceiveEnabled should be true after setUp");

vm.startPrank(admin);

// Enable receive while disabled
gateway.disable(bytes(""));
gateway.setIsReceiveEnabled(true);
assertTrue(gateway.isReceiveEnabled(), "isReceiveEnabled should be true");

// Re-enable, then disable again, isReceiveEnabled should be reset
gateway.enable(bytes(""));
gateway.disable(bytes(""));
assertFalse(gateway.isReceiveEnabled(), "disable should reset isReceiveEnabled");

vm.stopPrank();
}

function test_disable_revertsIfAlreadyDisabled() external {
vm.prank(admin);
gateway.disable(bytes(""));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {LZBridgeGatewayTestBase} from "src/test/policies/bridge/LZBridgeGateway/

// Interfaces
import {Origin} from "@lz-evm-protocol-v2-3.0.162/interfaces/ILayerZeroEndpointV2.sol";
import {IEnabler} from "src/periphery/interfaces/IEnabler.sol";
import {ILZBridgeGateway} from "src/policies/interfaces/ILZBridgeGateway.sol";

// Libraries
Expand Down Expand Up @@ -52,6 +51,36 @@ contract LZBridgeGatewayTests_LzReceive is LZBridgeGatewayTestBase {
);
}

function test_lzReceive_succeedsWhenBridgeDisabledButReceiveEnabled() external {
// Disable gateway2 but allow receiving
vm.startPrank(admin);
gateway2.disable(bytes(""));
gateway2.setIsReceiveEnabled(true);
vm.stopPrank();

assertFalse(gateway2.isEnabled(), "isEnabled should be false");
assertTrue(gateway2.isReceiveEnabled(), "isReceiveEnabled should be true");

// Send from canonical — gateway2 should still receive
_sendCanonicalToNonCanonical(recipient, 1000e9);

assertEq(ohm.balanceOf(recipient), 1000e9, "Recipient should receive OHM despite disabled");
}

function test_lzReceive_succeedsAfterBridgeDisableAndReceiveEnable() external {
vm.startPrank(admin);
gateway2.disable(bytes(""));
gateway2.setIsReceiveEnabled(true);
gateway2.enable(bytes(""));
vm.stopPrank();

assertTrue(gateway2.isReceiveEnabled(), "isReceiveEnabled should be true after re-enable");

_sendCanonicalToNonCanonical(recipient, 1000e9);

assertEq(ohm.balanceOf(recipient), 1000e9, "Recipient should receive OHM");
}

function testFuzz_lzReceive_revertsIfNotEndpoint(address caller_) external {
vm.assume(caller_ != address(endpointSetup.endpointList[1]));

Expand Down Expand Up @@ -207,7 +236,7 @@ contract LZBridgeGatewayTests_LzReceive is LZBridgeGatewayTestBase {
gateway2.lzReceive(origin, bytes32(0), bytes(""), address(0), bytes(""));
}

function test_lzReceive_revertsIfNotEnabled() external {
function test_lzReceive_revertsIfBridgeDisabledAndReceiveEnabledFalse() external {
vm.prank(admin);
gateway2.disable(bytes(""));

Expand All @@ -217,7 +246,9 @@ contract LZBridgeGatewayTests_LzReceive is LZBridgeGatewayTestBase {
nonce: 1
});

vm.expectRevert(abi.encodeWithSelector(IEnabler.NotEnabled.selector));
vm.expectRevert(
abi.encodeWithSelector(ILZBridgeGateway.LZBridgeGateway_ReceiveNotEnabled.selector)
);
vm.prank(address(endpointSetup.endpointList[1]));
gateway2.lzReceive(origin, bytes32(0), bytes(""), address(0), bytes(""));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,9 @@ contract LZBridgeGatewayTests_RetryingFailedMessages is LZBridgeGatewayTestBase_
// ========== DISABLED BRIDGE -> RE-ENABLE -> RETRY ========== //

/// @notice Destination gateway disabled. LZBridgeGateway.lzReceive() reverts
/// with NotEnabled (onlyEnabled modifier). Endpoint _clearPayload rolls
/// back, payload hash stays in storage. Re-enable and retry succeeds.
/// with ReceiveNotEnabled (isReceiveEnabled is false). Endpoint
/// _clearPayload rolls back, payload hash stays in storage. Re-enable
/// and retry succeeds.
function test_lzReceive_disabledBridgeSoReEnableAndRetry() external {
uint256 amount = 5000e9;

Expand Down Expand Up @@ -183,6 +184,37 @@ contract LZBridgeGatewayTests_RetryingFailedMessages is LZBridgeGatewayTestBase_
assertEq(ohm.balanceOf(recipient), amount, "Recipient should receive OHM after retry");
}

// ========== GATEWAY DISABLED -> SET RECEIVE ENABLED -> RETRY ========== //

/// @notice Gateway disabled after send. lzReceive() reverts with
/// ReceiveNotEnabled. Admin sets isReceiveEnabled, retry succeeds.
function test_lzReceive_gatewayDisabledSoSetReceiveEnabledAndRetry() external {
// 1. Send from canonical, packet enters mock queue
bytes memory packetBytes = _sendCanonicalNoDeliver(1000e9);

// 2. Disable destination gateway (isReceiveEnabled becomes false)
vm.prank(admin);
gateway2.disable(bytes(""));
assertFalse(gateway2.isReceiveEnabled(), "Receiving should be disabled");

// 3. Verify packet in endpoint (hash stored)
_verifyOnly(packetBytes);

// 4. Delivery fails (isReceiveEnabled == false)
bool delivered = _tryDeliverPacket(packetBytes);
assertFalse(delivered, "Delivery should fail while receiving is disabled");
assertEq(ohm.balanceOf(recipient), 0, "Recipient should have no OHM yet");

// 5. Admin enables receiving without re-enabling the gateway
vm.prank(admin);
gateway2.setIsReceiveEnabled(true);

// 6. Retry delivery succeeds
_manualDeliver(packetBytes, 1);

assertEq(ohm.balanceOf(recipient), 1000e9, "Recipient should receive OHM after retry");
}

// ========== INSUFFICIENT GAS IN ENFORCED OPTIONS ========== //

/// @notice Enforced options encode too little gas. EndpointV2.lzReceive() runs
Expand Down
Loading
Loading