From 692ec76a9edc3c4676bca764f817fac91499d364 Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 7 May 2026 10:03:19 -0400 Subject: [PATCH 1/4] add ConfigureL1OAppStakerForOP Gnosis bundle script Generates a Gnosis Safe transaction bundle for the HyperEVM controller to peer L1BeHYPEOAppStaker with the Optimism-side L2BeHYPEOAppStaker and configure send/receive DVNs on the HyperEVM endpoint for the OP route. Co-authored-by: Cursor --- script/oapp/ConfigureL1OAppStakerForOP.s.sol | 122 +++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 script/oapp/ConfigureL1OAppStakerForOP.s.sol diff --git a/script/oapp/ConfigureL1OAppStakerForOP.s.sol b/script/oapp/ConfigureL1OAppStakerForOP.s.sol new file mode 100644 index 0000000..fd85356 --- /dev/null +++ b/script/oapp/ConfigureL1OAppStakerForOP.s.sol @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import "forge-std/StdJson.sol"; + +import {ILayerZeroEndpointV2} from "lib/LayerZero-v2/packages/layerzero-v2/evm/protocol/contracts/interfaces/ILayerZeroEndpointV2.sol"; +import {IMessageLibManager, SetConfigParam} from "lib/LayerZero-v2/packages/layerzero-v2/evm/protocol/contracts/interfaces/IMessageLibManager.sol"; +import {UlnConfig} from "lib/LayerZero-v2/packages/layerzero-v2/evm/messagelib/contracts/uln/UlnBase.sol"; + +import {GnosisHelpers} from "../utils/GnosisHelpers.sol"; +import {L1BeHYPEOAppStaker} from "../../src/L1BeHYPEOAppStaker.sol"; + +/** + * @title ConfigureL1OAppStakerForOP + * @notice Generates a Gnosis Safe Transaction Builder JSON bundle for the HyperEVM + * controller (current L1BeHYPEOAppStaker owner) to peer the already-deployed + * L1BeHYPEOAppStaker with the new OP-side L2BeHYPEOAppStaker, and configure + * the HyperEVM endpoint send/receive DVNs for the OP route. + * + * The bundle is simulated on the current fork to surface any reverts before + * signing the multisig transaction. + * + * Bundle (executed by the L1 staker owner Safe): + * 1. L1BeHYPEOAppStaker.setPeer(OP_EID, L2BeHYPEOAppStaker) + * 2. endpoint.setConfig on send302 with [Nethermind, LayerZero] DVNs + * 3. endpoint.setConfig on receive302 with [Nethermind, LayerZero] DVNs + * + * forge script script/oapp/ConfigureL1OAppStakerForOP.s.sol:ConfigureL1OAppStakerForOP \ + * --rpc-url $HYPEREVM_RPC \ + * -vvvv + */ +contract ConfigureL1OAppStakerForOP is GnosisHelpers { + using stdJson for string; + + string constant OUTPUT_PATH = "output/hyperevm-l1-staker-op-config-bundle.json"; + string constant CHAIN_ID = "999"; + + function run() external { + string memory cfg = vm.readFile("config/production.json"); + + address l1Staker = cfg.readAddress(".layerZero.L1BeHYPEOAppStaker"); + // L2 staker is expected to land at the same CREATE3 address on OP as Scroll + // (per script/oapp/DeployAndConfigureOApps.s.sol). + address l2Staker = cfg.readAddress(".layerZero.L2BeHYPEOAppStaker"); + uint32 optimismEid = uint32(cfg.readUint(".layerZero.optimism.eid")); + + address endpoint = cfg.readAddress(".layerZero.hyperEVM.endpoint"); + address sendLib = cfg.readAddress(".layerZero.hyperEVM.send302"); + address receiveLib = cfg.readAddress(".layerZero.hyperEVM.receive302"); + address nevermindDvn = cfg.readAddress(".layerZero.hyperEVM.nevermindDvn"); + address layerZeroDvn = cfg.readAddress(".layerZero.hyperEVM.layerZeroDvn"); + + address safeAddress = L1BeHYPEOAppStaker(payable(l1Staker)).owner(); + + // --- 1. setPeer calldata --- + bytes memory setPeerData = abi.encodeWithSignature( + "setPeer(uint32,bytes32)", + optimismEid, + bytes32(uint256(uint160(l2Staker))) + ); + + // --- 2 & 3. setConfig calldata (send + receive) --- + address[] memory requiredDVNs = new address[](2); + if (layerZeroDvn > nevermindDvn) { + requiredDVNs[0] = nevermindDvn; + requiredDVNs[1] = layerZeroDvn; + } else { + requiredDVNs[0] = layerZeroDvn; + requiredDVNs[1] = nevermindDvn; + } + + UlnConfig memory ulnConfig = UlnConfig({ + confirmations: 30, + requiredDVNCount: 2, + optionalDVNCount: 0, + optionalDVNThreshold: 0, + requiredDVNs: requiredDVNs, + optionalDVNs: new address[](0) + }); + + SetConfigParam[] memory params = new SetConfigParam[](1); + params[0] = SetConfigParam(optimismEid, 2, abi.encode(ulnConfig)); + + bytes memory setConfigSendData = abi.encodeWithSelector( + IMessageLibManager.setConfig.selector, + l1Staker, + sendLib, + params + ); + + bytes memory setConfigReceiveData = abi.encodeWithSelector( + IMessageLibManager.setConfig.selector, + l1Staker, + receiveLib, + params + ); + + // --- Build Gnosis JSON bundle --- + string memory safeHex = addressToHex(safeAddress); + string memory l1StakerHex = addressToHex(l1Staker); + string memory endpointHex = addressToHex(endpoint); + + string memory bundle = string.concat( + _getGnosisHeader(CHAIN_ID, safeHex), + _getGnosisTransaction(l1StakerHex, iToHex(setPeerData), "0", false), + _getGnosisTransaction(endpointHex, iToHex(setConfigSendData), "0", false), + _getGnosisTransaction(endpointHex, iToHex(setConfigReceiveData), "0", true) + ); + + vm.writeFile(OUTPUT_PATH, bundle); + + // --- Simulate execution on fork --- + executeGnosisTransactionBundle(OUTPUT_PATH); + + // --- Verify --- + bytes32 peer = L1BeHYPEOAppStaker(payable(l1Staker)).peers(optimismEid); + require( + peer == bytes32(uint256(uint160(l2Staker))), + "Peer not set correctly" + ); + } +} From 17473ced2c3f8d9520728b0e1aa9c61005563f9f Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 7 May 2026 11:58:44 -0400 Subject: [PATCH 2/4] transfer L2 staker ownership to optimismController after deploy Reads optimismController from production config and calls setDelegate + transferOwnership at the end of the OP deploy branch, matching the Scroll staker which is owned by scrollController. Co-authored-by: Cursor --- script/oapp/DeployAndConfigureOApps.s.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/script/oapp/DeployAndConfigureOApps.s.sol b/script/oapp/DeployAndConfigureOApps.s.sol index 434b709..51ae597 100644 --- a/script/oapp/DeployAndConfigureOApps.s.sol +++ b/script/oapp/DeployAndConfigureOApps.s.sol @@ -63,7 +63,11 @@ contract DeployAndConfigureOApps is Script, Utils { cfg.readAddress(".layerZero.optimism.layerZeroDvn"), address(l2BeHYPEOAppStaker) ); - + + address opController = cfg.readAddress(".roles.optimismController"); + L2BeHYPEOAppStaker(l2BeHYPEOAppStaker).setDelegate(opController); + L2BeHYPEOAppStaker(l2BeHYPEOAppStaker).transferOwnership(opController); + } else { L1BeHYPEOAppStaker l1Impl = new L1BeHYPEOAppStaker(cfg.readAddress(".layerZero.hyperEVM.endpoint")); l1BeHYPEOAppStaker = _deployL1OAppStaker(address(l1Impl), owner); From 7de32d1b7c6e7dc968cd4716be4570562b4a75bb Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 7 May 2026 13:02:48 -0400 Subject: [PATCH 3/4] Add bytecode verification for L2BeHYPEOAppStaker on Optimism MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Follows the pattern of OFTOptimismVerification — verifies implementation bytecode, owner, LZ delegate, and HyperEVM peer configuration. Co-authored-by: Cursor --- .../oapp/OAppStakerOptimismVerification.s.sol | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 script/oapp/OAppStakerOptimismVerification.s.sol diff --git a/script/oapp/OAppStakerOptimismVerification.s.sol b/script/oapp/OAppStakerOptimismVerification.s.sol new file mode 100644 index 0000000..b37bd3a --- /dev/null +++ b/script/oapp/OAppStakerOptimismVerification.s.sol @@ -0,0 +1,79 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.24; + +import {Test, console} from "forge-std/Test.sol"; +import {ContractCodeChecker} from "../utils/ContractCodeChecker.sol"; +import {L2BeHYPEOAppStaker} from "../../src/L2BeHYPEOAppStaker.sol"; +import "forge-std/StdJson.sol"; + +interface IEndpointV2 { + function delegates(address oapp) external view returns (address); +} + +interface IOAppCore { + function peers(uint32 eid) external view returns (bytes32); +} + +/** + * @title OAppStakerOptimismVerification + * @notice Bytecode verification for the L2BeHYPEOAppStaker deployed on Optimism. + * + * forge test --match-contract OAppStakerOptimismVerification -vvv + */ +contract OAppStakerOptimismVerification is ContractCodeChecker, Test { + using stdJson for string; + + bytes32 constant IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + function test_verifyL2BeHYPEOAppStakerOnOptimism() public { + vm.createSelectFork("https://mainnet.optimism.io"); + + string memory config = vm.readFile("config/production.json"); + + address l2StakerProxy = config.readAddress(".layerZero.L2BeHYPEOAppStaker"); + address optimismEndpoint = config.readAddress(".layerZero.optimism.endpoint"); + address expectedController = config.readAddress(".roles.optimismController"); + uint32 hyperEVMEid = uint32(config.readUint(".layerZero.hyperEVM.eid")); + + console.log("=== L2BeHYPEOAppStaker on Optimism Verification ==="); + console.log("Proxy:", l2StakerProxy); + + // --- Implementation Bytecode --- + console.log("--- Verifying Implementation Bytecode ---"); + address impl = _getImplementationAddress(l2StakerProxy); + console.log("Implementation:", impl); + + address localImpl = address(new L2BeHYPEOAppStaker(optimismEndpoint)); + verifyContractByteCodeMatch(impl, localImpl); + + // --- Config & Roles --- + L2BeHYPEOAppStaker staker = L2BeHYPEOAppStaker(payable(l2StakerProxy)); + + assertEq(staker.owner(), expectedController, "owner != optimismController"); + console.log("Owner is optimismController"); + + assertEq( + IEndpointV2(optimismEndpoint).delegates(l2StakerProxy), + expectedController, + "delegate != optimismController" + ); + console.log("LZ delegate is optimismController"); + + // Verify peer is set to L1 staker on HyperEVM + address l1Staker = config.readAddress(".layerZero.L1BeHYPEOAppStaker"); + bytes32 expectedPeer = bytes32(uint256(uint160(l1Staker))); + assertEq( + IOAppCore(l2StakerProxy).peers(hyperEVMEid), + expectedPeer, + "HyperEVM peer not set correctly" + ); + console.log("HyperEVM peer verified"); + + console.log("=== L2BeHYPEOAppStaker Optimism Verification Complete ==="); + } + + function _getImplementationAddress(address proxy) internal view returns (address) { + bytes32 slotValue = vm.load(proxy, IMPLEMENTATION_SLOT); + return address(uint160(uint256(slotValue))); + } +} From e9964cc5c381e63843281479e0909fa17b8322df Mon Sep 17 00:00:00 2001 From: jtfirek Date: Thu, 7 May 2026 13:15:51 -0400 Subject: [PATCH 4/4] Add generated HyperEVM L1 staker OP config Gnosis bundle Co-authored-by: Cursor --- output/hyperevm-l1-staker-op-config-bundle.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 output/hyperevm-l1-staker-op-config-bundle.json diff --git a/output/hyperevm-l1-staker-op-config-bundle.json b/output/hyperevm-l1-staker-op-config-bundle.json new file mode 100644 index 0000000..6625184 --- /dev/null +++ b/output/hyperevm-l1-staker-op-config-bundle.json @@ -0,0 +1 @@ +{"chainId":"999","safeAddress":"0xf27128a5b064e8d97edaa60d24bfa2fd1eec26eb","meta": { "txBuilderVersion": "1.16.5" }, "transactions": [{"to":"0xe6a3bcd93e9d56e5252db47ff512629fd3cc9b54","value":"0","data":"0x3400288b000000000000000000000000000000000000000000000000000000000000759f0000000000000000000000003f1bdae959ced680e434fe201861e97976ea4a8f"},{"to":"0x3a73033c0b1407574c76bdbac67f126f6b4a9aa9","value":"0","data":"0x6dbd9f90000000000000000000000000e6a3bcd93e9d56e5252db47ff512629fd3cc9b54000000000000000000000000fd76d9cb0bac839725ab79127e7411fe71b1e3ca000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000759f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000020000000000000000000000008e49ef1dfae17e547ca0e7526ffda81fbaca810a000000000000000000000000c097ab8cd7b053326dfe9fb3e3a31a0cce3b526f0000000000000000000000000000000000000000000000000000000000000000"},{"to":"0x3a73033c0b1407574c76bdbac67f126f6b4a9aa9","value":"0","data":"0x6dbd9f90000000000000000000000000e6a3bcd93e9d56e5252db47ff512629fd3cc9b540000000000000000000000007cacbe439ead55fa1c22790330b12835c6884a91000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000759f0000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001e00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000012000000000000000000000000000000000000000000000000000000000000000020000000000000000000000008e49ef1dfae17e547ca0e7526ffda81fbaca810a000000000000000000000000c097ab8cd7b053326dfe9fb3e3a31a0cce3b526f0000000000000000000000000000000000000000000000000000000000000000"}]} \ No newline at end of file