Skip to content

Commit 459e2ed

Browse files
authored
feat: Refactor config script to prepare set bridge DVN task (#92)
1 parent 6463463 commit 459e2ed

File tree

7 files changed

+464
-29
lines changed

7 files changed

+464
-29
lines changed

script/bridges/layerZero/IexecLayerZeroBridge.s.sol

Lines changed: 121 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH <[email protected]>
22
// SPDX-License-Identifier: Apache-2.0
3+
34
pragma solidity ^0.8.22;
45

6+
import {console} from "forge-std/console.sol";
57
import {Script} from "forge-std/Script.sol";
68
import {Upgrades} from "openzeppelin-foundry-upgrades/Upgrades.sol";
9+
import {IAccessControl} from "@openzeppelin/contracts/access/IAccessControl.sol";
710
import {EnforcedOptionParam} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
811
import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
912
import {ConfigLib} from "./../../lib/ConfigLib.sol";
@@ -12,11 +15,17 @@ import {RLCLiquidityUnifier} from "../../../src/RLCLiquidityUnifier.sol";
1215
import {RLCCrosschainToken} from "../../../src/RLCCrosschainToken.sol";
1316
import {UUPSProxyDeployer} from "../../lib/UUPSProxyDeployer.sol";
1417
import {UpgradeUtils} from "../../lib/UpgradeUtils.sol";
18+
import {LayerZeroUtils} from "../../utils/LayerZeroUtils.sol";
1519

20+
/**
21+
* A script to deploy and initialize the IexecLayerZeroBridge contract.
22+
* It uses CreateX to deploy the contract as a UUPS proxy.
23+
*/
1624
contract Deploy is Script {
1725
/**
18-
* Reads configuration from config file and deploys IexecLayerZeroBridge contract.
19-
* @return address of the deployed IexecLayerZeroBridge proxy contract.
26+
* Reads configuration from config file and deploys `IexecLayerZeroBridge` contract.
27+
* @dev This function is called by `forge script run`.
28+
* @return address of the deployed `IexecLayerZeroBridge` proxy contract.
2029
*/
2130
function run() external returns (address) {
2231
string memory chain = vm.envString("CHAIN");
@@ -59,39 +68,131 @@ contract Deploy is Script {
5968
}
6069
}
6170

71+
/**
72+
* A script to configure the IexecLayerZeroBridge contract:
73+
* - Set the peer for the bridge (`setPeer`).
74+
* - Set enforced options for the bridge (`setEnforcedOptions`).
75+
* - Set DVNs config for the bridge (`setDvnConfig`). TODO
76+
* - Authorize the bridge in RLCLiquidityUnifier or RLCCrosschainToken contract (`grantRole`).
77+
* The script should be called at least once for each chain where the bridge is configured.
78+
*/
6279
contract Configure is Script {
6380
using OptionsBuilder for bytes;
6481

82+
/**
83+
* Reads configuration from config file and configures `IexecLayerZeroBridge` contract.
84+
* @dev This function is called by `forge script run`.
85+
*/
6586
function run() external {
6687
string memory sourceChain = vm.envString("SOURCE_CHAIN");
6788
string memory targetChain = vm.envString("TARGET_CHAIN");
6889
ConfigLib.CommonConfigParams memory sourceParams = ConfigLib.readCommonConfig(sourceChain);
6990
ConfigLib.CommonConfigParams memory targetParams = ConfigLib.readCommonConfig(targetChain);
70-
IexecLayerZeroBridge sourceBridge = IexecLayerZeroBridge(sourceParams.iexecLayerZeroBridgeAddress);
91+
console.log("Configuring bridge [chain:%s, address:%s]", sourceChain, sourceParams.iexecLayerZeroBridgeAddress);
7192
vm.startBroadcast();
72-
sourceBridge.setPeer(
73-
targetParams.lzEndpointId, bytes32(uint256(uint160(targetParams.iexecLayerZeroBridgeAddress)))
93+
configure(sourceParams, targetParams);
94+
vm.stopBroadcast();
95+
}
96+
97+
/**
98+
* Setup bridge configuration.
99+
* @param sourceParams Configuration parameters for the source chain.
100+
* @param targetParams Configuration parameters for the target chain.
101+
* @return true if at least one configuration was changed, false otherwise.
102+
*/
103+
function configure(
104+
ConfigLib.CommonConfigParams memory sourceParams,
105+
ConfigLib.CommonConfigParams memory targetParams
106+
) public returns (bool) {
107+
address bridge = sourceParams.iexecLayerZeroBridgeAddress;
108+
RLCLiquidityUnifier rlcLiquidityUnifier = RLCLiquidityUnifier(sourceParams.rlcLiquidityUnifierAddress);
109+
RLCCrosschainToken rlcCrosschainToken = RLCCrosschainToken(sourceParams.rlcCrosschainTokenAddress);
110+
bool bool1 = setBridgePeerIfNeeded(bridge, targetParams.lzEndpointId, targetParams.iexecLayerZeroBridgeAddress);
111+
bool bool2 = setEnforcedOptionsIfNeeded(bridge, targetParams.lzEndpointId);
112+
bool bool3 = authorizeBridgeIfNeeded(
113+
bridge,
114+
sourceParams.approvalRequired ? address(rlcLiquidityUnifier) : address(rlcCrosschainToken),
115+
sourceParams.approvalRequired
116+
? rlcLiquidityUnifier.TOKEN_BRIDGE_ROLE()
117+
: rlcCrosschainToken.TOKEN_BRIDGE_ROLE()
74118
);
75-
EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2);
76-
bytes memory _extraOptions = OptionsBuilder.newOptions().addExecutorLzReceiveOption(90_000, 0); // 90_000 gas limit for the receiving executor and 0 for the executor's value
77-
enforcedOptions[0] = EnforcedOptionParam(targetParams.lzEndpointId, 1, _extraOptions); // lzReceive
78-
enforcedOptions[1] = EnforcedOptionParam(targetParams.lzEndpointId, 2, _extraOptions); // lzCompose
79-
sourceBridge.setEnforcedOptions(enforcedOptions);
80-
// Authorize bridge in the relevant contract.
81-
if (sourceParams.approvalRequired) {
82-
RLCLiquidityUnifier rlcLiquidityUnifier = RLCLiquidityUnifier(sourceParams.rlcLiquidityUnifierAddress);
83-
bytes32 bridgeTokenRoleId = rlcLiquidityUnifier.TOKEN_BRIDGE_ROLE();
84-
rlcLiquidityUnifier.grantRole(bridgeTokenRoleId, address(sourceBridge));
85-
} else {
86-
RLCCrosschainToken rlcCrosschainToken = RLCCrosschainToken(sourceParams.rlcCrosschainTokenAddress);
87-
bytes32 bridgeTokenRoleId = rlcCrosschainToken.TOKEN_BRIDGE_ROLE();
88-
rlcCrosschainToken.grantRole(bridgeTokenRoleId, address(sourceBridge));
119+
return bool1 || bool2 || bool3;
120+
}
121+
122+
/**
123+
* Sets the bridge peer if it is not already set. Otherwise, do nothing.
124+
* @dev see https://docs.layerzero.network/v2/developers/evm/technical-reference/integration-checklist#call-setpeer-on-every-oapp-deployment
125+
* @param bridgeAddress The address of the LayerZero bridge contract.
126+
* @param targetEndpointId The ID of the target LayerZero endpoint.
127+
* @param targetBridgeAddress The address of the target LayerZero bridge contract.
128+
*/
129+
function setBridgePeerIfNeeded(address bridgeAddress, uint32 targetEndpointId, address targetBridgeAddress)
130+
public
131+
returns (bool)
132+
{
133+
IexecLayerZeroBridge bridge = IexecLayerZeroBridge(bridgeAddress);
134+
bytes32 peer = bytes32(uint256(uint160(targetBridgeAddress)));
135+
if (bridge.isPeer(targetEndpointId, peer)) {
136+
console.log(
137+
"Bridge peer already set [endpointId:%s, peer:%s]", vm.toString(targetEndpointId), vm.toString(peer)
138+
);
139+
return false;
89140
}
141+
console.log("Setting bridge peer [endpointId:%s, peer:%s]", vm.toString(targetEndpointId), vm.toString(peer));
142+
bridge.setPeer(targetEndpointId, peer);
143+
return true;
144+
}
90145

91-
vm.stopBroadcast();
146+
/**
147+
* Sets the enforced options for the LayerZero bridge if they are not already set.
148+
* If the same options are already enforced on-chain, do nothing.
149+
* @dev see https://docs.layerzero.network/v2/developers/evm/technical-reference/integration-checklist#implement-enforced-options
150+
* @param bridgeAddress The address of the LayerZero bridge contract.
151+
* @param targetEndpointId The ID of the target LayerZero endpoint.
152+
*/
153+
function setEnforcedOptionsIfNeeded(address bridgeAddress, uint32 targetEndpointId) public returns (bool) {
154+
IexecLayerZeroBridge bridge = IexecLayerZeroBridge(bridgeAddress);
155+
bytes memory options = LayerZeroUtils.buildLzReceiveExecutorConfig(90_000, 0);
156+
if (LayerZeroUtils.matchesOnchainEnforcedOptions(bridge, targetEndpointId, options)) {
157+
console.log(
158+
"Bridge enforced options already set [endpointId:%s, options:%s]",
159+
vm.toString(targetEndpointId),
160+
vm.toString(options)
161+
);
162+
return false;
163+
}
164+
EnforcedOptionParam[] memory enforcedOptions = LayerZeroUtils.buildEnforcedOptions(targetEndpointId, options);
165+
console.log(
166+
"Setting bridge enforced options [endpointId:%s, options:%s]",
167+
vm.toString(targetEndpointId),
168+
vm.toString(options)
169+
);
170+
bridge.setEnforcedOptions(enforcedOptions);
171+
return true;
172+
}
173+
174+
/**
175+
* Authorizes the bridge in the RLCLiquidityUnifier or RLCCrosschainToken contract if it
176+
* is not already authorized. Otherwise, do nothing.
177+
* @param bridge The address of the LayerZero bridge contract.
178+
* @param authorizerAddress The address of the authorizer contract.
179+
* @param roleId The role ID to grant to the bridge.
180+
*/
181+
function authorizeBridgeIfNeeded(address bridge, address authorizerAddress, bytes32 roleId) public returns (bool) {
182+
IAccessControl authorizer = IAccessControl(authorizerAddress);
183+
if (authorizer.hasRole(roleId, bridge)) {
184+
console.log("Bridge already authorized");
185+
return false;
186+
}
187+
console.log("Granting bridge role in contract", authorizerAddress);
188+
authorizer.grantRole(roleId, bridge);
189+
return true;
92190
}
93191
}
94192

193+
/**
194+
* A script to upgrade the IexecLayerZeroBridge contract.
195+
*/
95196
contract Upgrade is Script {
96197
function run() external {
97198
string memory chain = vm.envString("CHAIN");

script/utils/LayerZeroUtils.sol

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
// SPDX-FileCopyrightText: 2025 IEXEC BLOCKCHAIN TECH <[email protected]>
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
pragma solidity ^0.8.22;
5+
6+
import {Vm} from "forge-std/Vm.sol";
7+
import {console} from "forge-std/console.sol";
8+
import {stdJson} from "forge-std/StdJson.sol";
9+
import {OptionsBuilder} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OptionsBuilder.sol";
10+
import {EnforcedOptionParam} from "@layerzerolabs/oapp-evm/contracts/oapp/libs/OAppOptionsType3.sol";
11+
import {IexecLayerZeroBridge} from "../../src/bridges/layerZero/IexecLayerZeroBridge.sol";
12+
13+
// TODO move script/lib/* utility files in this folder.
14+
library LayerZeroUtils {
15+
using OptionsBuilder for bytes;
16+
17+
uint16 constant LZ_RECEIVE_MESSAGE_TYPE = 1; // lzReceive()
18+
uint16 constant LZ_COMPOSE_MESSAGE_TYPE = 2; // lzCompose()
19+
20+
/**
21+
* Builds the LayerZero receive configuration for the executor.
22+
* @param gasLimit The gas limit for the lzReceive() function.
23+
* @param value The msg.value for the lzReceive() function.
24+
*/
25+
function buildLzReceiveExecutorConfig(uint128 gasLimit, uint128 value) public pure returns (bytes memory) {
26+
return OptionsBuilder.newOptions().addExecutorLzReceiveOption(gasLimit, value);
27+
}
28+
29+
/**
30+
* Gets the on-chain enforced options for `lzReceive()`.
31+
* @param bridge The LayerZero bridge contract.
32+
* @param endpointId The LayerZero endpoint ID of the target chain.
33+
*/
34+
function getOnchainLzReceiveEnforcedOptions(IexecLayerZeroBridge bridge, uint32 endpointId)
35+
public
36+
view
37+
returns (bytes memory)
38+
{
39+
return bridge.enforcedOptions(endpointId, LZ_RECEIVE_MESSAGE_TYPE);
40+
}
41+
42+
/**
43+
* Gets the on-chain enforced options for `lzCompose()`.
44+
* @param bridge The LayerZero bridge contract.
45+
* @param endpointId The LayerZero endpoint ID of the target chain.
46+
*/
47+
function getOnchainLzComposeEnforcedOptions(IexecLayerZeroBridge bridge, uint32 endpointId)
48+
public
49+
view
50+
returns (bytes memory)
51+
{
52+
return bridge.enforcedOptions(endpointId, LZ_COMPOSE_MESSAGE_TYPE);
53+
}
54+
55+
/**
56+
* Checks if the on-chain options for the bridge match the provided options for the target chain.
57+
* @param bridge The source bridge contract.
58+
* @param endpointId The LayerZero endpoint ID of the target chain.
59+
* @param options The options to compare against.
60+
*/
61+
function matchesOnchainEnforcedOptions(IexecLayerZeroBridge bridge, uint32 endpointId, bytes memory options)
62+
public
63+
view
64+
returns (bool)
65+
{
66+
bytes memory lzReceiveOnchainOptions = getOnchainLzReceiveEnforcedOptions(bridge, endpointId);
67+
bytes memory lzComposeOnchainOptions = getOnchainLzComposeEnforcedOptions(bridge, endpointId);
68+
return keccak256(lzReceiveOnchainOptions) == keccak256(options)
69+
&& keccak256(lzComposeOnchainOptions) == keccak256(options);
70+
}
71+
72+
/**
73+
* Builds the enforced options for a LayerZero bridge.
74+
* @param targetEndpointId The LayerZero endpoint ID of the receiving chain.
75+
* @param options The options to enforce.
76+
*/
77+
function buildEnforcedOptions(uint32 targetEndpointId, bytes memory options)
78+
public
79+
pure
80+
returns (EnforcedOptionParam[] memory)
81+
{
82+
EnforcedOptionParam[] memory enforcedOptions = new EnforcedOptionParam[](2);
83+
enforcedOptions[0] = EnforcedOptionParam(targetEndpointId, LZ_RECEIVE_MESSAGE_TYPE, options);
84+
enforcedOptions[1] = EnforcedOptionParam(targetEndpointId, LZ_COMPOSE_MESSAGE_TYPE, options);
85+
return enforcedOptions;
86+
}
87+
}

src/bridges/layerZero/IexecLayerZeroBridge.sol

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ contract IexecLayerZeroBridge is
192192
* @dev Overridden to prevent ownership renouncement.
193193
* AccessControlDefaultAdminRulesUpgradeable is used to manage ownership.
194194
*/
195+
// TODO make this as a non-view function.
195196
function renounceOwnership() public pure override {
196197
revert OperationNotAllowed("Use AccessControlDefaultAdminRulesUpgradeable instead");
197198
}
@@ -200,6 +201,7 @@ contract IexecLayerZeroBridge is
200201
* @dev Overridden to prevent ownership transfer.
201202
* AccessControlDefaultAdminRulesUpgradeable is used to manage ownership.
202203
*/
204+
// TODO make this as a non-view function.
203205
function transferOwnership(address) public pure override {
204206
revert OperationNotAllowed("Use AccessControlDefaultAdminRulesUpgradeable instead");
205207
}

test/e2e/IexecLayerZeroBridgeScript.t.sol renamed to test/e2e/IexecLayerZeroBridgeDeployScript.t.sol

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {RLCLiquidityUnifier} from "../../src/RLCLiquidityUnifier.sol";
1414
import {RLCCrosschainToken} from "../../src/RLCCrosschainToken.sol";
1515
import {ConfigLib} from "../../script/lib/ConfigLib.sol";
1616

17-
contract IexecLayerZeroBridgeScriptTest is Test {
17+
// TODO move to test/units/bridges/layerzero/
18+
contract IexecLayerZeroBridgeDeployScriptTest is Test {
1819
// The chain does not matter here as the LAYERZERO_ENDPOINT address is the same for both networks (Sepolia & Arbitrum Sepolia)
1920
ConfigLib.CommonConfigParams params = ConfigLib.readCommonConfig("sepolia");
2021

@@ -67,8 +68,8 @@ contract IexecLayerZeroBridgeScriptTest is Test {
6768

6869
function _test_Deployment(bool requireApproval, address bridgeableToken) internal {
6970
// Check that CreateX salt is used to deploy the contract.
70-
vm.expectEmit(false, true, false, false);
7171
// CreateX uses a guarded salt (see CreateX._guard()), so we need to hash it to match the expected event.
72+
vm.expectEmit(false, true, false, false);
7273
emit CreateX.ContractCreation(address(0), keccak256(abi.encode(salt)));
7374
IexecLayerZeroBridge iexecLayerZeroBridge = IexecLayerZeroBridge(
7475
deployer.deploy(
@@ -97,7 +98,8 @@ contract IexecLayerZeroBridgeScriptTest is Test {
9798
// Make sure the contract has been initialized and cannot be re-initialized.
9899
vm.expectRevert(abi.encodeWithSelector(Initializable.InvalidInitialization.selector));
99100
iexecLayerZeroBridge.initialize(admin, upgrader, pauser);
100-
// TODO check that the contract has the correct LayerZero endpoint.
101+
// Make sure the contract has the correct LayerZero endpoint.
102+
assertEq(address(iexecLayerZeroBridge.endpoint()), params.lzEndpoint, "Incorrect LayerZero endpoint address");
101103
// TODO check that the proxy address is saved.
102104
}
103105

0 commit comments

Comments
 (0)