diff --git a/script/20260706-deploy-tokens-ethereum.s.sol b/script/20260706-deploy-tokens-ethereum.s.sol index 42f7c9d4..06bf47ee 100644 --- a/script/20260706-deploy-tokens-ethereum.s.sol +++ b/script/20260706-deploy-tokens-ethereum.s.sol @@ -212,7 +212,7 @@ contract DeployTokensEthereum is Script { // pins, NOT `LibProdDeployCurrent`: Ethereum's bootstrap shipped the // audited 0.1.1 set, and the 0.1.1 unified deployer is what wires // new vault proxies onto the chain's IN-USE production beacons (the - // 0.1.1 set pinned via `LibProdBeaconsEthereum` / + // 0.1.1 set pinned via `LibProdBeacons0_1_1` / // `LibBeaconInvariants`). The current tag's deployer is a different // Zoltu address that (a) is not deployed on Ethereum and (b) would // wire tokens onto a different, unadopted beacon set even if it were. diff --git a/script/20260716-migrate-beacon-owners-ethereum.s.sol b/script/20260716-migrate-beacon-owners-ethereum.s.sol index 2ad69358..1b08b943 100644 --- a/script/20260716-migrate-beacon-owners-ethereum.s.sol +++ b/script/20260716-migrate-beacon-owners-ethereum.s.sol @@ -5,7 +5,7 @@ pragma solidity =0.8.25; import {Script} from "forge-std-1.16.1/src/Script.sol"; import {console2} from "forge-std-1.16.1/src/console2.sol"; import {Ownable} from "@openzeppelin-contracts-5.6.1/access/Ownable.sol"; -import {LibProdBeaconsEthereum} from "../src/lib/LibProdBeaconsEthereum.sol"; +import {LibProdBeacons0_1_1} from "../src/lib/LibProdBeacons0_1_1.sol"; import {LibProdDeployV1} from "../src/lib/LibProdDeployV1.sol"; import {LibSafeInvariants} from "../src/lib/LibSafeInvariants.sol"; import {LibBeaconInvariants} from "../src/lib/LibBeaconInvariants.sol"; @@ -39,8 +39,8 @@ contract MigrateBeaconOwnersEthereum is Script { function run() external { address safe = LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_ETHEREUM; require(safe != address(0), "Ethereum token-owner Safe not pinned"); - address[3] memory beaconList = LibProdBeaconsEthereum.beacons(); - address[3] memory implList = LibProdBeaconsEthereum.implementations(); + address[3] memory beaconList = LibProdBeacons0_1_1.beacons(); + address[3] memory implList = LibProdBeacons0_1_1.implementations(); // Pre-flight: every beacon is deployed, is the OZ UpgradeableBeacon, // is still owned by the deploy EOA, and points at its pinned impl. diff --git a/script/20260722-migrate-beacon-owners-hyperevm.s.sol b/script/20260722-migrate-beacon-owners-hyperevm.s.sol new file mode 100644 index 00000000..70ba9146 --- /dev/null +++ b/script/20260722-migrate-beacon-owners-hyperevm.s.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Script} from "forge-std-1.16.1/src/Script.sol"; +import {console2} from "forge-std-1.16.1/src/console2.sol"; +import {Ownable} from "@openzeppelin-contracts-5.6.1/access/Ownable.sol"; +import {LibProdBeacons0_1_1} from "../src/lib/LibProdBeacons0_1_1.sol"; +import {LibProdDeployV1} from "../src/lib/LibProdDeployV1.sol"; +import {LibSafeInvariants} from "../src/lib/LibSafeInvariants.sol"; +import {LibBeaconInvariants} from "../src/lib/LibBeaconInvariants.sol"; + +/// @title MigrateBeaconOwnersHyperEvm +/// @notice **EXECUTED 2026-07-24** (broadcast from the deploy EOA's CLI; +/// all three beacons verified Safe-owned with implementations unchanged). +/// Transfers ownership of the three ST0x production +/// beacons on **HyperEVM** (chain id 999) from the deploy EOA +/// (`LibProdDeployV1.BEACON_INITIAL_OWNER`, rainlang.eth) to the HyperEVM +/// token-owner Safe (`LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_HYPEREVM`). +/// The HyperEVM leg of the same migration executed for Base (#253) and +/// Ethereum (`20260716-migrate-beacon-owners-ethereum`) — HyperEVM +/// bootstraps at 0.1.1, so its beacons are the SAME deterministic addresses +/// as Ethereum's (`LibProdBeacons0_1_1`). +/// +/// @dev This is a **deploy-EOA broadcast**, not a Safe artifact: the beacons +/// come up EOA-owned from the beacon-set deployer constructors, so the +/// migration is the EOA calling `transferOwnership`. Broadcast as the EOA: +/// +/// forge script script/20260722-migrate-beacon-owners-hyperevm.s.sol \ +/// --rpc-url hyperevm --legacy --broadcast --private-key +/// +/// (`--legacy`: HyperEVM's RPC rejects forge's EIP-1559 `eth_feeHistory` +/// fee-estimation ranges.) +/// +/// Ordering (RAI-1511): run AFTER the 0.1.1 impl suites are deployed on +/// HyperEVM (the beacons don't exist before) and AFTER the HyperEVM Safe +/// pin hydrates, and BEFORE the token deploy — the token-deploy pre-flight +/// hard-gates on `assertProdBeaconsOwnedByChainSafe`. The beacons back no +/// vaults yet at that point, so this is pure ownership hand-off with no +/// live-vault risk. +contract MigrateBeaconOwnersHyperEvm is Script { + /// @notice Pre-flight every beacon against the EOA-owned state, broadcast + /// the three `transferOwnership` calls to the HyperEVM Safe, then + /// re-assert every beacon against the Safe-owned state. Implementations + /// are asserted unchanged across the transfer. + function run() external { + address safe = LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_HYPEREVM; + require(safe != address(0), "HyperEVM token-owner Safe not pinned"); + require(block.chainid == LibSafeInvariants.HYPEREVM_CHAIN_ID, "not HyperEVM - wrong --rpc-url"); + address[3] memory beaconList = LibProdBeacons0_1_1.beacons(); + address[3] memory implList = LibProdBeacons0_1_1.implementations(); + + // Pre-flight: every beacon is deployed, is the OZ UpgradeableBeacon, + // is still owned by the deploy EOA, and points at its pinned impl. + // Reverts with the relevant typed error on the first drift, before any + // broadcast happens. + for (uint256 i = 0; i < beaconList.length; i++) { + LibBeaconInvariants.assertBeaconInvariants(beaconList[i], LibProdDeployV1.BEACON_INITIAL_OWNER, implList[i]); + } + + // Broadcast the ownership transfers from the EOA — three separate + // transactions, one per beacon. + vm.startBroadcast(); + for (uint256 i = 0; i < beaconList.length; i++) { + Ownable(beaconList[i]).transferOwnership(safe); + } + vm.stopBroadcast(); + + // Post-state: every beacon is now Safe-owned, implementations + // unchanged. + for (uint256 i = 0; i < beaconList.length; i++) { + LibBeaconInvariants.assertBeaconInvariants(beaconList[i], safe, implList[i]); + } + + console2.log("Transferred ownership of 3 HyperEVM beacons to:", vm.toString(safe)); + } +} diff --git a/src/lib/LibBeaconInvariants.sol b/src/lib/LibBeaconInvariants.sol index 25402bb0..86cc8482 100644 --- a/src/lib/LibBeaconInvariants.sol +++ b/src/lib/LibBeaconInvariants.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.25; import {IBeacon} from "@openzeppelin-contracts-5.6.1/proxy/beacon/IBeacon.sol"; import {LibProdBeaconsBase} from "./LibProdBeaconsBase.sol"; -import {LibProdBeaconsEthereum} from "./LibProdBeaconsEthereum.sol"; +import {LibProdBeacons0_1_1} from "./LibProdBeacons0_1_1.sol"; import {LibSafeInvariants} from "./LibSafeInvariants.sol"; /// @notice Minimal `Ownable`-like surface used to read a beacon's owner. @@ -184,7 +184,7 @@ library LibBeaconInvariants { /// chain's production tokens point at them — only the implementations they /// serve are upgraded — so "which beacons is production running on" is /// per-chain pinned state. Each chain's set lives in its own lib - /// (`LibProdBeaconsBase` / `LibProdBeaconsEthereum`, same shape and index + /// (`LibProdBeaconsBase` / `LibProdBeacons0_1_1`, same shape and index /// order); this map only dispatches by chain id. /// @param chainId The active chain id (`block.chainid`). /// @return The chain's three in-use beacon addresses. @@ -193,7 +193,12 @@ library LibBeaconInvariants { return LibProdBeaconsBase.beacons(); } if (chainId == LibSafeInvariants.ETHEREUM_CHAIN_ID) { - return LibProdBeaconsEthereum.beacons(); + return LibProdBeacons0_1_1.beacons(); + } + if (chainId == LibSafeInvariants.HYPEREVM_CHAIN_ID) { + // HyperEVM bootstraps at 0.1.1 too — the deterministic beacon + // set resolves to the SAME addresses as Ethereum's. + return LibProdBeacons0_1_1.beacons(); } revert UnsupportedChainForProdBeacons(chainId); } diff --git a/src/lib/LibProdBeaconsEthereum.sol b/src/lib/LibProdBeacons0_1_1.sol similarity index 81% rename from src/lib/LibProdBeaconsEthereum.sol rename to src/lib/LibProdBeacons0_1_1.sol index b6f31ca1..ae095151 100644 --- a/src/lib/LibProdBeaconsEthereum.sol +++ b/src/lib/LibProdBeacons0_1_1.sol @@ -5,12 +5,15 @@ pragma solidity ^0.8.25; import {IST0xVaultBeaconSet} from "../interface/IST0xVaultBeaconSet.sol"; import {LibProdDeployV4} from "../generated/LibProdDeployV4.sol"; -/// @title LibProdBeaconsEthereum -/// @notice The three ST0x production beacons on **Ethereum mainnet** and the -/// implementations they point at — every address traced to the generated -/// `0_1_1` pins rather than re-pasted as fresh literals. -/// @dev Ethereum bootstrapped fresh at the **0.1.1** release, so its beacons -/// and impls are the `0_1_1` deployment. Two principles keep this lib free of +/// @title LibProdBeacons0_1_1 +/// @notice The three ST0x production beacons of the deterministic **0.1.1** +/// deployment and the implementations they point at — every address traced +/// to the generated `0_1_1` pins rather than re-pasted as fresh literals. +/// The whole set is Zoltu-deterministic, so these are the SAME addresses on +/// every chain that bootstraps at 0.1.1 (Ethereum mainnet; HyperEVM per +/// RAI-1511). +/// @dev A 0.1.1-bootstrap chain's beacons and impls are the `0_1_1` +/// deployment. Two principles keep this lib free of /// pasted addresses: /// /// 1. **Implementations are chain-agnostic.** They are deployed @@ -36,7 +39,10 @@ import {LibProdDeployV4} from "../generated/LibProdDeployV4.sol"; /// Ethereum migration (`20260716-migrate-beacon-owners-ethereum`) transfers /// them to `LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_ETHEREUM`, mirroring what /// `MigrateBeaconOwners` already did for Base's beacons. -library LibProdBeaconsEthereum { +// The version-suffixed name mirrors the generated `0_1_1` pin naming that +// this lib exists to trace; CapWords would obscure the version. +// slither-disable-next-line naming-convention +library LibProdBeacons0_1_1 { /// @notice The three production beacons, in a fixed order (receipt, /// receipt vault, wrapped token vault) — index-aligned with /// `implementations()`. The receipt / receipt-vault beacons are read from diff --git a/src/lib/LibProdBeaconsBase.sol b/src/lib/LibProdBeaconsBase.sol index 0c5fcd1c..23457613 100644 --- a/src/lib/LibProdBeaconsBase.sol +++ b/src/lib/LibProdBeaconsBase.sol @@ -8,7 +8,7 @@ import {LibProdDeployV4} from "../generated/LibProdDeployV4.sol"; /// @title LibProdBeaconsBase /// @notice The three ST0x production beacons on **Base** and the /// implementations they point at — the Base counterpart of -/// `LibProdBeaconsEthereum`, same shape and index order so per-chain +/// `LibProdBeacons0_1_1`, same shape and index order so per-chain /// consumers dispatch to one lib per chain instead of hand-assembling /// either side. /// @dev Base's production tokens run on the **V1-generation** beacon @@ -27,7 +27,7 @@ import {LibProdDeployV4} from "../generated/LibProdDeployV4.sol"; library LibProdBeaconsBase { /// @notice The three production beacons, in a fixed order (receipt, /// receipt vault, wrapped token vault) — index-aligned with - /// `implementations()` and with `LibProdBeaconsEthereum.beacons()`. + /// `implementations()` and with `LibProdBeacons0_1_1.beacons()`. /// @return The three Base beacon addresses. function beacons() internal pure returns (address[3] memory) { return [ @@ -39,7 +39,7 @@ library LibProdBeaconsBase { /// @notice The implementation each beacon points at, index-aligned with /// `beacons()`. Referenced from the generated `0_1_1` impl pins — the - /// same deterministic addresses `LibProdBeaconsEthereum.implementations()` + /// same deterministic addresses `LibProdBeacons0_1_1.implementations()` /// resolves, because implementation parity across chains is the goal. /// @return The three implementation addresses. function implementations() internal pure returns (address[3] memory) { diff --git a/test/src/concrete/deploy/EthereumBeaconOwnership.t.sol b/test/src/concrete/deploy/EthereumBeaconOwnership.t.sol index 35f588ce..b78fa48d 100644 --- a/test/src/concrete/deploy/EthereumBeaconOwnership.t.sol +++ b/test/src/concrete/deploy/EthereumBeaconOwnership.t.sol @@ -3,7 +3,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibProdBeaconsEthereum} from "../../../../src/lib/LibProdBeaconsEthereum.sol"; +import {LibProdBeacons0_1_1} from "../../../../src/lib/LibProdBeacons0_1_1.sol"; import {LibProdDeployV1} from "../../../../src/lib/LibProdDeployV1.sol"; import {LibSafeInvariants} from "../../../../src/lib/LibSafeInvariants.sol"; import {LibBeaconInvariants} from "../../../../src/lib/LibBeaconInvariants.sol"; @@ -31,8 +31,8 @@ contract EthereumBeaconOwnershipTest is Test { } vm.createSelectFork(LibStoxDeployNetworks.ETHEREUM); - address[3] memory beacons = LibProdBeaconsEthereum.beacons(); - address[3] memory impls = LibProdBeaconsEthereum.implementations(); + address[3] memory beacons = LibProdBeacons0_1_1.beacons(); + address[3] memory impls = LibProdBeacons0_1_1.implementations(); for (uint256 i = 0; i < beacons.length; i++) { LibBeaconInvariants.assertBeaconInvariants(beacons[i], safe, impls[i]); } diff --git a/test/src/concrete/deploy/HyperEvmBeaconOwnership.t.sol b/test/src/concrete/deploy/HyperEvmBeaconOwnership.t.sol new file mode 100644 index 00000000..0e5ebf4e --- /dev/null +++ b/test/src/concrete/deploy/HyperEvmBeaconOwnership.t.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: LicenseRef-DCL-1.0 +// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd +pragma solidity =0.8.25; + +import {Test} from "forge-std-1.16.1/src/Test.sol"; +import {LibProdBeacons0_1_1} from "../../../../src/lib/LibProdBeacons0_1_1.sol"; +import {LibSafeInvariants} from "../../../../src/lib/LibSafeInvariants.sol"; +import {LibBeaconInvariants} from "../../../../src/lib/LibBeaconInvariants.sol"; +import {LibStoxDeployNetworks} from "../../../../src/lib/LibStoxDeployNetworks.sol"; + +/// @title HyperEvmBeaconOwnershipTest +/// @notice The forcing function for the HyperEVM beacon-ownership migration +/// (`20260722-migrate-beacon-owners-hyperevm`), mirroring +/// `EthereumBeaconOwnershipTest`: every chain's production beacons must be +/// owned by that chain's token-owner Safe. RED from the moment the 0.1.1 +/// impl suites land on HyperEVM (beacons come up EOA-owned) until the +/// migration runs; green thereafter, catching later ownership drift. +/// +/// @dev The invariant runs unconditionally: the HyperEVM token-owner Safe is +/// pinned in `LibSafeInvariants`, and CI supplies `HYPEREVM_RPC_URL` to the +/// shared rainix test workflow from the `RPC_URL_HYPEREVM_FORK` secret, so the +/// fork always resolves. +contract HyperEvmBeaconOwnershipTest is Test { + /// Every HyperEVM beacon is owned by the HyperEVM token-owner Safe (with + /// the OZ beacon codehash + its pinned impl unchanged). RED until the + /// migration transfers ownership from the deploy EOA to the Safe. + function testHyperEvmBeaconsAreSafeOwned() external { + address safe = LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_HYPEREVM; + + vm.createSelectFork(LibStoxDeployNetworks.HYPEREVM); + address[3] memory beacons = LibProdBeacons0_1_1.beacons(); + address[3] memory impls = LibProdBeacons0_1_1.implementations(); + for (uint256 i = 0; i < beacons.length; i++) { + LibBeaconInvariants.assertBeaconInvariants(beacons[i], safe, impls[i]); + } + } +} diff --git a/test/src/lib/LibBeaconInvariants.t.sol b/test/src/lib/LibBeaconInvariants.t.sol index 481e9aa8..082987f0 100644 --- a/test/src/lib/LibBeaconInvariants.t.sol +++ b/test/src/lib/LibBeaconInvariants.t.sol @@ -14,7 +14,7 @@ import { } from "../../../src/lib/LibBeaconInvariants.sol"; import {LibSafeInvariants} from "../../../src/lib/LibSafeInvariants.sol"; import {LibProdBeaconsBase} from "../../../src/lib/LibProdBeaconsBase.sol"; -import {LibProdBeaconsEthereum} from "../../../src/lib/LibProdBeaconsEthereum.sol"; +import {LibProdBeacons0_1_1} from "../../../src/lib/LibProdBeacons0_1_1.sol"; import {LibProdDeployV1} from "../../../src/lib/LibProdDeployV1.sol"; import {LibStoxDeployNetworks} from "../../../src/lib/LibStoxDeployNetworks.sol"; import {LibBeaconInvariantsHarness} from "./LibBeaconInvariantsHarness.sol"; @@ -135,7 +135,7 @@ contract LibBeaconInvariantsTest is Test { vm.createSelectFork(LibStoxDeployNetworks.ETHEREUM); harness = new LibBeaconInvariantsHarness(); address[3] memory beacons = harness.callProdBeaconsForChainId(LibSafeInvariants.ETHEREUM_CHAIN_ID); - address[3] memory expected = LibProdBeaconsEthereum.beacons(); + address[3] memory expected = LibProdBeacons0_1_1.beacons(); assertEq(beacons[0], expected[0], "receipt beacon"); assertEq(beacons[1], expected[1], "receipt vault beacon"); assertEq(beacons[2], expected[2], "wrapped vault beacon");