diff --git a/.github/workflows/manual-broadcast.yaml b/.github/workflows/manual-broadcast.yaml index d1138aa4..3601df07 100644 --- a/.github/workflows/manual-broadcast.yaml +++ b/.github/workflows/manual-broadcast.yaml @@ -34,6 +34,7 @@ on: # `base`. - 'base' - 'ethereum' + - 'hyperevm' # Dispatches an operational broadcast script from `script/` and sends the # resulting transactions from the CI deploy key (`secrets.PRIVATE_KEY`, # the same secret `manual-sol-artifacts.yaml` uses for Zoltu impl deploys). @@ -76,6 +77,7 @@ jobs: # one foundry.toml resolves via its `[rpc_endpoints]` aliases. BASE_RPC_URL: ${{ secrets.RPC_URL_BASE_FORK }} ETHEREUM_RPC_URL: ${{ secrets.RPC_URL_ETHEREUM_FORK }} + HYPEREVM_RPC_URL: ${{ secrets.RPC_URL_HYPEREVM_FORK }} # Pass the choice inputs via env rather than template-expanding them # into the command, so the dispatched values are used as literal # arguments and cannot inject shell. @@ -86,7 +88,14 @@ jobs: # persist it anywhere else. PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} run: | + # HyperEVM's RPC rejects forge's EIP-1559 fee-history estimation; + # type-0 transactions work on every network. + LEGACY_ARGS=() + if [[ "${NETWORK}" == "hyperevm" ]]; then + LEGACY_ARGS+=(--legacy) + fi nix develop --command forge script "script/${SCRIPT}.s.sol" \ + "${LEGACY_ARGS[@]}" \ --sig 'run()' \ --rpc-url "${NETWORK}" \ --no-storage-caching \ diff --git a/.github/workflows/manual-sol-artifacts-ethereum-0-1-1.yaml b/.github/workflows/manual-sol-artifacts-0-1-1.yaml similarity index 64% rename from .github/workflows/manual-sol-artifacts-ethereum-0-1-1.yaml rename to .github/workflows/manual-sol-artifacts-0-1-1.yaml index 66bec846..a78474e4 100644 --- a/.github/workflows/manual-sol-artifacts-ethereum-0-1-1.yaml +++ b/.github/workflows/manual-sol-artifacts-0-1-1.yaml @@ -1,4 +1,8 @@ -name: Manual sol artifacts (Ethereum 0.1.1) +name: Manual sol artifacts (0.1.1 bootstrap) +# Least-privilege: the broadcast authenticates via `secrets.PRIVATE_KEY`, so +# the ambient GITHUB_TOKEN needs nothing beyond read. +permissions: + contents: read on: workflow_dispatch: inputs: @@ -8,8 +12,9 @@ on: type: choice options: # =================================================================== - # The audited 0.1.1 production set, shipped to Ethereum mainnet only - # via script/DeployProdV4_0_1_1.sol (stored 0.1.1 creation bytecode). + # The audited 0.1.1 production set, shipped to the selected bootstrap + # network via script/DeployProdV4_0_1_1.sol (stored 0.1.1 creation + # bytecode). # The orchestrator (introduced at 0.1.2) is not part of this set. # # Deploy in the listed order; later entries reference earlier ones via @@ -33,20 +38,34 @@ on: - stox-offchain-asset-receipt-vault-beacon-set-deployer # 5. Unified deployer (depends on the two set-deployers above). - stox-unified-deployer + network: + description: 'Bootstrap network to ship the 0.1.1 set to' + required: true + type: choice + options: + # Closed set of vetted bootstrap networks — mirrored by the deploy + # script's `bootstrapNetwork()` guard. + - 'ethereum' + - 'hyperevm' jobs: deploy: uses: rainlanguage/rainix/.github/workflows/rainix-manual-sol-artifacts.yaml@main with: suite: ${{ inputs.suite }} + network: ${{ inputs.network }} + # HyperEVM's RPC rejects forge's EIP-1559 fee-history estimation; + # type-0 transactions work on every bootstrap network. + legacy: ${{ inputs.network == 'hyperevm' }} # Ship the stored 0.1.1 creation bytecode, not current source. script: script/DeployProdV4_0_1_1.sol:Deploy # The 0.1.1 bytecode of the contracts that changed by 0.1.3 does not match # current source, so Etherscan verification against current source would # fail the run. Deploy unverified; verify manually from the 0.1.1 tag. verify: false - # This script targets Ethereum only and runs unverified, so it needs only the - # deployer key and the Ethereum RPC. The reusable declares every other secret - # as optional and falls back to `|| vars.* || ''` for any it isn't given. + # Runs unverified, so it needs only the deployer key and the bootstrap + # networks' RPCs. The reusable declares every other secret as optional and + # falls back to `|| vars.* || ''` for any it isn't given. secrets: PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} RPC_URL_ETHEREUM_FORK: ${{ secrets.RPC_URL_ETHEREUM_FORK }} + RPC_URL_HYPEREVM_FORK: ${{ secrets.RPC_URL_HYPEREVM_FORK }} diff --git a/foundry.toml b/foundry.toml index 09ed2187..8d76b35a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -72,6 +72,7 @@ base = "${BASE_RPC_URL}" base_sepolia = "${BASE_SEPOLIA_RPC_URL}" ethereum = "${ETHEREUM_RPC_URL}" flare = "${FLARE_RPC_URL}" +hyperevm = "${HYPEREVM_RPC_URL}" polygon = "${POLYGON_RPC_URL}" [etherscan] diff --git a/script/DeployProdV4_0_1_1.sol b/script/DeployProdV4_0_1_1.sol index db497255..95bf4183 100644 --- a/script/DeployProdV4_0_1_1.sol +++ b/script/DeployProdV4_0_1_1.sol @@ -12,10 +12,15 @@ import {LibStoxDeployNetworks} from "../src/lib/LibStoxDeployNetworks.sol"; /// suite. error UnknownDeploymentSuite(bytes32 suite); +/// @dev Error thrown when the DEPLOYMENT_NETWORK env var does not match any +/// supported bootstrap network. +error UnknownDeploymentNetwork(string network); + // One suite per contract to avoid Zoltu factory nonce issues. // -// This script ships the audited 0.1.1 production set to Ethereum mainnet only. -// Unlike `script/Deploy.sol`, which deploys the CURRENT source (the 0.1.3 pins) +// This script ships the audited 0.1.1 production set to one bootstrap network +// per dispatch, selected by `DEPLOYMENT_NETWORK`. Unlike +// `script/Deploy.sol`, which deploys the CURRENT source (the 0.1.3 pins) // to `LibStoxDeployNetworks.supportedNetworks()`, each suite here deploys the // stored `LibProdDeployV4.*_CREATION_CODE_0_1_1` bytecode — the exact bytes the // 0.1.1 audit covers — and asserts against the `_0_1_1` address/codehash pins. @@ -42,8 +47,9 @@ bytes32 constant DEPLOYMENT_SUITE_STOX_OFFCHAIN_ASSET_RECEIPT_VAULT_PAYMENT_MINT bytes32 constant DEPLOYMENT_SUITE_STOX_CORPORATE_ACTIONS_FACET = keccak256("stox-corporate-actions-facet"); contract Deploy is Script { - /// @dev Broadcasts a single contract via the Zoltu deterministic deployer on - /// Ethereum mainnet. Reads `DEPLOYMENT_KEY` from the environment, logs + /// @dev Broadcasts a single contract via the Zoltu deterministic deployer + /// on the selected bootstrap network. Reads `DEPLOYMENT_KEY` from the + /// environment, logs /// diagnostic information (expected address, codehash, dependency state), /// then delegates to `LibRainDeploy.deployAndBroadcast`. /// @param creationCode The creation bytecode of the contract to deploy. @@ -53,8 +59,8 @@ contract Deploy is Script { /// to. /// @param expectedCodeHash The expected codehash of the deployed runtime /// bytecode. - /// @param dependencies Addresses of contracts that must already be deployed - /// on Ethereum before this contract is deployed. + /// @param dependencies Addresses of contracts that must already be + /// deployed on the selected network before this contract is deployed. function deploySuite( bytes memory creationCode, string memory contractPath, @@ -63,7 +69,7 @@ contract Deploy is Script { address[] memory dependencies ) internal { string[] memory networks = new string[](1); - networks[0] = LibStoxDeployNetworks.ETHEREUM; + networks[0] = bootstrapNetwork(); uint256 deployerPrivateKey = vm.envUint("DEPLOYMENT_KEY"); console2.log("Suite deploying (0.1.1):", contractPath); @@ -92,11 +98,29 @@ contract Deploy is Script { ); } - /// @notice Entry point for the 0.1.1 Ethereum deployment script. + /// @notice The bootstrap network the suite broadcasts to, from the + /// `DEPLOYMENT_NETWORK` env var, validated against the supported set. + /// No default: an unset network reverts rather than silently picking a + /// chain. + /// @return network The validated `foundry.toml` rpc alias. + function bootstrapNetwork() internal view returns (string memory network) { + network = vm.envOr("DEPLOYMENT_NETWORK", string("")); + bytes32 networkHash = keccak256(bytes(network)); + if ( + networkHash != keccak256(bytes(LibStoxDeployNetworks.ETHEREUM)) + && networkHash != keccak256(bytes(LibStoxDeployNetworks.HYPEREVM)) + ) { + revert UnknownDeploymentNetwork(network); + } + } + + /// @notice Entry point for the 0.1.1 bootstrap deployment script. /// @dev Requires env vars: /// - `DEPLOYMENT_KEY`: private key for the deployer account. /// - `DEPLOYMENT_SUITE`: which contract to deploy (e.g. "stox-receipt"). /// One contract per run. + /// - `DEPLOYMENT_NETWORK`: bootstrap network to ship to — `ethereum` or + /// `hyperevm`. function run() public { bytes32 suite = keccak256(bytes(vm.envString("DEPLOYMENT_SUITE"))); address[] memory noDeps = new address[](0); @@ -114,7 +138,7 @@ contract Deploy is Script { // hardcoded corporate-actions facet, and a delegatecall to a // code-less address silently no-ops — so the facet must already be // on-chain. Declared as a dependency so LibRainDeploy reverts - // MissingDependency if the facet is not yet deployed on Ethereum. + // MissingDependency if the facet is not yet deployed on the network. address[] memory deps = new address[](1); deps[0] = LibProdDeployV4.STOX_CORPORATE_ACTIONS_FACET_0_1_1; deploySuite( diff --git a/src/lib/LibStoxDeployNetworks.sol b/src/lib/LibStoxDeployNetworks.sol index 88640087..b99438a8 100644 --- a/src/lib/LibStoxDeployNetworks.sol +++ b/src/lib/LibStoxDeployNetworks.sol @@ -24,6 +24,14 @@ library LibStoxDeployNetworks { /// deploys work unchanged. string internal constant ETHEREUM = "ethereum"; + /// @notice HyperEVM mainnet network name, matching the `[rpc_endpoints]` + /// alias in `foundry.toml` (resolved from `HYPEREVM_RPC_URL`). + /// @dev The Zoltu factory is deployed on HyperEVM at the canonical + /// `LibRainDeploy.ZOLTU_FACTORY` address, so deterministic deploys work + /// unchanged. Not in `supportedNetworks()`: HyperEVM carries the audited + /// 0.1.1 set, not the current source `script/Deploy.sol` ships. + string internal constant HYPEREVM = "hyperevm"; + /// @notice The networks each suite in `script/Deploy.sol` is broadcast to. /// @return networks The list of network names (Base + Ethereum mainnet). function supportedNetworks() internal pure returns (string[] memory networks) {