diff --git a/src/lib/LibTokenInvariants.sol b/src/lib/LibTokenInvariants.sol index d50366ff..bc802be6 100644 --- a/src/lib/LibTokenInvariants.sol +++ b/src/lib/LibTokenInvariants.sol @@ -298,7 +298,7 @@ library LibTokenInvariants { /// the flat `productionReceiptVaults()` accessor derives from; consumers /// that need the receipt / wrapped-vault legs or the underlying join key /// (cross-chain parity, per-token config checks) iterate this instead. - /// @return tokens The 28 production token instances on Base. + /// @return tokens The 29 production token instances on Base. function productionTokensBase() internal pure returns (TokenInstance[] memory tokens) { tokens = new TokenInstance[](29); tokens[0] = TokenInstance("MSTR", MSTR_RECEIPT, MSTR_RECEIPT_VAULT, MSTR_WRAPPED_TOKEN_VAULT); @@ -333,16 +333,9 @@ library LibTokenInvariants { } /// @notice Returns the production token instance triples on Ethereum - /// mainnet — the same 28 underlyings as Base, in the same order, so the + /// mainnet — the same 29 underlyings as Base, in the same order, so the /// two tables pair by index as well as by key. - /// - /// **ALL PLACEHOLDERS** (`address(0)`) until the Ethereum token - /// deployments execute and the post-execution pin PR hydrates every entry - /// in one reviewed change. Hydration is all-or-nothing across the table: - /// the multichain issuance cutover is lockstep over the full token set, so - /// a partially hydrated table is an error state, which the cross-chain - /// parity suite rejects rather than half-checks. - /// @return tokens The 28 production token instances on Ethereum. + /// @return tokens The 29 production token instances on Ethereum. function productionTokensEthereum() internal pure returns (TokenInstance[] memory tokens) { // Deployed on Ethereum mainnet 2026-07-22 by // `20260706-deploy-tokens-ethereum` (manual-broadcast run @@ -534,13 +527,195 @@ library LibTokenInvariants { ); } - /// @notice Returns the 28 production receipt vault addresses on Base, in + /// @notice Returns the 29 production token instance triples on HyperEVM, + /// in the same row order as `productionTokensBase()` (the cross-chain + /// parity pin asserts the alignment). + /// @return tokens The 29 production token instances on HyperEVM. + function productionTokensHyperEvm() internal pure returns (TokenInstance[] memory tokens) { + tokens = new TokenInstance[](29); + tokens[0] = TokenInstance( + "MSTR", + 0xE3772C8695c2cf3dcAA2Dd29759f4Bb91a342763, + 0x8500189061e2206Bc33Bf04DC10fFB1Fe7dED637, + 0xd9fE7488B86D3aEaf457b181C744BD1A5a120833 + ); + tokens[1] = TokenInstance( + "TSLA", + 0x3a3E00d6fb65E686941f77DD375ca677Ad7772c2, + 0xB41fD00d0bA60D9Ae8dCE405cB6AAd5710E5F84d, + 0x550499e28A3CE8cb1dB3e3Db23fDD0357eD3ff48 + ); + tokens[2] = TokenInstance( + "COIN", + 0x5cF43A3fFd5B979C36B6512800cdAa6A2EF4f352, + 0x5100ED387Ab3ED37667199aD8f9F6D963157d28e, + 0xa3d1be1Ab9F4E72309Cb9Bfeaa14aFe46D012e36 + ); + tokens[3] = TokenInstance( + "SPYM", + 0xB023d277f4a50cE056Dd921C28fF7e27F56B3445, + 0x484AaA9e6542774026b24aeD4EC3058400eD2439, + 0x7dF3ad1ECC10DBF0296D05F91a697e4A059771c2 + ); + tokens[4] = TokenInstance( + "SIVR", + 0x89dC2d5B33e8DcbD24A2e4A07f7B06f55b5bb24e, + 0xc6100518997004eFb0701Da3c56000B3d093470a, + 0x2B310218001C38cf82816c2098AedDCE22D3Df27 + ); + tokens[5] = TokenInstance( + "CRCL", + 0xDf228279B380e8445970a3B4162B9509D4339520, + 0xeF63EdB9F39Dcd03C103A11e2e7E1878308b9586, + 0x25Ab6926BA171e1618b214cda774104B2f6ec884 + ); + tokens[6] = TokenInstance( + "NVDA", + 0xA26C89357e6cb53Bf670db2e0e939f5489Ba26A8, + 0xf6A89b0c9FF897000E37bBD06397992278FfC50d, + 0xa3947d2A74F5a3A7135A9d0c66B28fC7315f30Da + ); + tokens[7] = TokenInstance( + "IAU", + 0x9003CAc529d1299891C2aaa08b4c810b81F49488, + 0xb9A1D1822F57f52959b8c5097A8322D534bceDEe, + 0x33f3a6401ce08f705eE4B8a03fc37F298395b720 + ); + tokens[8] = TokenInstance( + "PPLT", + 0x216cbEeC16cF7e4dBd53e6e3D8B54b7dA23BE146, + 0x47C2e6644eFDF58E86dA45dC60e0f67A65043B99, + 0xCbD06C802BFe993de94d0d635AB5B1fa764519D8 + ); + tokens[9] = TokenInstance( + "AMZN", + 0xA583addaA69142E8588D9ffE3767Dbb0F23fd516, + 0x6615f3D82989949fa7d167b40FEc0Ef30cdbA476, + 0x4404E24629a33b85FC1E2D7beA673Cd283054594 + ); + tokens[10] = TokenInstance( + "BMNR", + 0x34BF824C28121EbD421026D70a03A7Ec9Ee5a0d4, + 0x00472aA0D0611F933c22b8148F02B0cDd1Ae5fbc, + 0x35e1D3d3Fa6D43438641d2b8f63cBf84C84435c0 + ); + tokens[11] = TokenInstance( + "IBHG", + 0x8e05CB17994Ee9B207e87993717F6Fb2ee7D0bD3, + 0x36b30F5B5D1AcD3D8135Afe7a5516A300021f139, + 0x03bBb4148ba82d63993D32fB8E6Eff8Cc51A56f2 + ); + tokens[12] = TokenInstance( + "SGOV", + 0xE4B5Af0bAc3dC97b77d6eE4E95F59c74c679ee5c, + 0x344147366F648640076d363FAF659c214788E99d, + 0x06b17E431a957Dd8522Bf106653BAc6B39F437E6 + ); + tokens[13] = TokenInstance( + "QQQM", + 0x1Eb9583d1BB2b00B3CB5ead921d021b445F77dBc, + 0x5Aa65dfF455C7f18C21370086EaFaEe4f2b63608, + 0x63fDfe9cf53cB1FD5d98fE7537B881B86aE04A31 + ); + tokens[14] = TokenInstance( + "VWO", + 0x1A09A154E2ae3890d1e18507a37bC3e2FCfA10cc, + 0x2cc8DCfC649f9633C482C81473cC251226375fE3, + 0x01EE8b582147D1Aa8A8f2Adc2EBB91fA60082AB0 + ); + tokens[15] = TokenInstance( + "ARKK", + 0xF1E5f11e7fAA40b2C2CEdfAD825D81207f793862, + 0xDf406836D5A092894ee5d5bdC7F58e5bdc8D196A, + 0x5D80cAcaABCe0ab4C1541493ceB25D97970D250a + ); + tokens[16] = TokenInstance( + "SPCX", + 0x46dC08972b1d5D5876d244292c551dbbbC7d7e80, + 0x659Ea9dd1C833fc76E5328E0E616d8b9D9836dc3, + 0xC32C8166164E2cA18BB5fbf3E82CB776943d97FB + ); + tokens[17] = TokenInstance( + "CEG", + 0x99Ffbf38E7c0D9C440BA629D0108E7d0E96cB710, + 0x6240E93f1A08d43002e52b210cD49c54C813D780, + 0xc842fC56Dd39B62357d027a0F3B3266353cac991 + ); + tokens[18] = TokenInstance( + "DRAM", + 0xbD4Fa8Fd2643b46eADC499b5A468108817988830, + 0x5F5a1Bdc00ade7702ea5944B1374DC598dA3759f, + 0x7e1263f1e1EeF7eCe7fA7e568842f213a6CEa956 + ); + tokens[19] = TokenInstance( + "TSM", + 0x2Ce6A6B9E573ec0444C00103ACEee9dC23223621, + 0x7Ac659f601bEa2d9f490E0D0D8a68c4282fD9B21, + 0x050dB961D36DC2047c779798694AeE455CD40BE6 + ); + tokens[20] = TokenInstance( + "SKHY", + 0xd3150728FFDb338eFd9b07ae90A7D5e688bc3B20, + 0xa3a7BEcF428b250Ec1b88Dbc65F579a9570670c7, + 0xD13bBc46C4582246656868d32227A99A538D97E1 + ); + tokens[21] = TokenInstance( + "ASML", + 0x6AEBd84df448d2E25080590797075b399Ca43549, + 0x77Ee94C8B85cF48a426F84B0F2d796eC77e41c7b, + 0xb8605a64B57E7A8631c87F3187c783493231bdD9 + ); + tokens[22] = TokenInstance( + "MU", + 0x98A3887f51AA93079978D265Cb20E215AfEB8b0f, + 0x2C845ed32c5fDE012eb14508d0d24BD3300B1D71, + 0xF01523C4ca52dF88f07938D4Fb32FCBb6b159867 + ); + tokens[23] = TokenInstance( + "AMD", + 0x0e257A952817ef779407aA27E5CD5F27863883Aa, + 0xe2De878d8Ca6bA544BFE80a10fAb0395DF32Cff0, + 0xb904955bC71FE6c8e6aBE676ACca640Abc771622 + ); + tokens[24] = TokenInstance( + "AVGO", + 0x607991FE92F9476eBedA4761295E5E2cc37e2834, + 0x503a69b2918DFA152eb0e0803174ed90ba5be756, + 0xCee4981Fc273C105e2b9eA1fD19031Aa9a6cf113 + ); + tokens[25] = TokenInstance( + "AMAT", + 0x8C8810F83a0F5a8C1A3D541411100DBFB68C5b0c, + 0x26139195f7a82a52c62C4047F843C99c61Ed9a5E, + 0x44DE27A07B33CD708A49fa79629fAC1df03C56d3 + ); + tokens[26] = TokenInstance( + "LRCX", + 0x2630CA3D952b265F88C189f3679FA985dF34A93D, + 0xe68A46547CdBBB181587B32cCD1A505Bedf0a994, + 0xD1c1D974837d3f79A92E14048993AD28a6228831 + ); + tokens[27] = TokenInstance( + "TTWO", + 0x47E3487B12272Bb1F933452F855CEb1C5e27204E, + 0xb62E913f0cC881862527Fa7e41e1C98eEf09cedD, + 0x1D6F0763e58FA6d472d470Eaaef0a4C08080d208 + ); + tokens[28] = TokenInstance( + "RKLB", + 0xFf5b15a4f478F296893b0b244D9b118Be87bCda2, + 0xED0c085d92C262FB46937CB0B3C9763Af7fCCf30, + 0x8FC87Be766C0cB6f254F1FDc9351D4B85B560FB3 + ); + } + + /// @notice Returns the 29 production receipt vault addresses on Base, in /// the order they were deployed. Provided so consumers (e.g. invariant /// assertions, migration scripts) can iterate without hardcoding the /// list inline. /// @dev Derived from `productionTokensBase()` so the token table is the /// single source of truth and the two accessors cannot drift. - /// @return vaults The 28 production receipt vault addresses on Base. + /// @return vaults The 29 production receipt vault addresses on Base. function productionReceiptVaults() internal pure returns (address[] memory vaults) { TokenInstance[] memory tokens = productionTokensBase(); vaults = new address[](tokens.length); diff --git a/test/src/concrete/deploy/StoxCrossChainParity.t.sol b/test/src/concrete/deploy/StoxCrossChainParity.t.sol index 0e4150b4..8148756a 100644 --- a/test/src/concrete/deploy/StoxCrossChainParity.t.sol +++ b/test/src/concrete/deploy/StoxCrossChainParity.t.sol @@ -380,6 +380,12 @@ contract StoxCrossChainParityTest is Test { /// point is that the choice is made deliberately rather than by silence. uint256 internal constant ETHEREUM_PARITY_DEADLINE = 1_790_812_800; + /// @notice Unix timestamp past which the HyperEVM legs must have armed — + /// `2026-11-01T00:00:00Z`, tracking the RAI-1511 bootstrap. PLACEHOLDER + /// in the same sense as the Ethereum deadline: move it deliberately if + /// the bootstrap slips. + uint256 internal constant HYPEREVM_PARITY_DEADLINE = 1_793_491_200; + /// @notice Assert every LIVE leg of a chain on the ACTIVE fork, skipping /// (with a loud PENDING log) any leg whose pins are still placeholders, and /// capture what it read for the cross-chain comparison. The legs are nested @@ -502,6 +508,18 @@ contract StoxCrossChainParityTest is Test { LibTokenInvariants.productionTokensEthereum() ); + // HyperEVM forks unconditionally, like Base and Ethereum: CI supplies + // `HYPEREVM_RPC_URL` to the shared rainix test workflow from the + // `RPC_URL_HYPEREVM_FORK` secret, so a missing RPC fails at fork time + // rather than leaving the legs unasserted behind a green run. + vm.createSelectFork(LibStoxDeployNetworks.HYPEREVM); + ChainLegs memory hyper = assertChainLegs( + "HyperEVM", + LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_HYPEREVM, + LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE_HYPEREVM, + LibTokenInvariants.productionTokensHyperEvm() + ); + // ---- Cross-chain comparisons, each gated on both sides being live ---- // Safe policy: same owner SET (order-insensitive) + threshold, compared @@ -551,6 +569,62 @@ contract StoxCrossChainParityTest is Test { } } + // HyperEVM vs Base — the same comparisons as Ethereum's, gated on + // each leg being live on both sides. + if (base.safeLive && hyper.safeLive) { + assertEq(hyper.threshold, base.threshold, "Safe threshold diverges cross-chain (HyperEVM)"); + assertSameOwnerSet(base.owners, hyper.owners); + } + if (base.cloneLive && hyper.cloneLive) { + assertEq( + hyper.cloneCodehash, + base.cloneCodehash, + "authoriser clone impl codehash diverges cross-chain (HyperEVM)" + ); + } + if (base.tokenLegLive && hyper.tokenLegLive) { + assertEq(hyper.beaconImpl, base.beaconImpl, "receipt-vault beacon impl diverges cross-chain (HyperEVM)"); + assertEq( + hyper.beaconImplCodehash, + base.beaconImplCodehash, + "receipt-vault beacon impl codehash diverges cross-chain (HyperEVM)" + ); + assertEq( + hyper.receiptBeaconImpl, base.receiptBeaconImpl, "receipt beacon impl diverges cross-chain (HyperEVM)" + ); + assertEq( + hyper.receiptBeaconImplCodehash, + base.receiptBeaconImplCodehash, + "receipt beacon impl codehash diverges cross-chain (HyperEVM)" + ); + assertEq(base.tokenConfigs.length, hyper.tokenConfigs.length, "token table lengths diverge (HyperEVM)"); + for (uint256 i = 0; i < base.tokenConfigs.length; i++) { + TokenConfigSnapshot memory b = base.tokenConfigs[i]; + TokenConfigSnapshot memory h = hyper.tokenConfigs[i]; + assertEq(h.underlying, b.underlying, "token table underlying order diverges (HyperEVM)"); + assertEq(h.vaultName, b.vaultName, string.concat(b.underlying, ": vault name diverges (HyperEVM)")); + assertEq( + h.vaultSymbol, b.vaultSymbol, string.concat(b.underlying, ": vault symbol diverges (HyperEVM)") + ); + assertEq( + h.vaultDecimals, b.vaultDecimals, string.concat(b.underlying, ": vault decimals diverge (HyperEVM)") + ); + assertEq( + h.wrappedName, b.wrappedName, string.concat(b.underlying, ": wrapped name diverges (HyperEVM)") + ); + assertEq( + h.wrappedSymbol, + b.wrappedSymbol, + string.concat(b.underlying, ": wrapped symbol diverges (HyperEVM)") + ); + assertEq( + h.wrappedDecimals, + b.wrappedDecimals, + string.concat(b.underlying, ": wrapped decimals diverge (HyperEVM)") + ); + } + } + // ---- The suite must prove it actually ran ---- // Every comparison above is gated on both chains carrying the leg, so a @@ -576,5 +650,13 @@ contract StoxCrossChainParityTest is Test { assertTrue(eth.cloneLive, "Ethereum authoriser leg still pending past the parity deadline"); assertTrue(eth.tokenLegLive, "Ethereum token leg still pending past the parity deadline"); } + + // HyperEVM's legs arm as the RAI-1511 bootstrap lands. Same forcing + // function as Ethereum's. + if (block.timestamp >= HYPEREVM_PARITY_DEADLINE) { + assertTrue(hyper.safeLive, "HyperEVM Safe leg still pending past the parity deadline"); + assertTrue(hyper.cloneLive, "HyperEVM authoriser leg still pending past the parity deadline"); + assertTrue(hyper.tokenLegLive, "HyperEVM token leg still pending past the parity deadline"); + } } } diff --git a/test/src/concrete/deploy/StoxProdV4.t.sol b/test/src/concrete/deploy/StoxProdV4.t.sol index f3a1fc4d..6700e02e 100644 --- a/test/src/concrete/deploy/StoxProdV4.t.sol +++ b/test/src/concrete/deploy/StoxProdV4.t.sol @@ -210,4 +210,17 @@ contract StoxProdV4Test is Test { checkProd_0_1_1OnChain(); LibBeaconInvariants.assertProdBeaconsOwnedByChainSafe(block.chainid); } + + /// Only the audited 0.1.1 production set ships to HyperEVM (the RAI-1511 + /// bootstrap), mirroring Ethereum. The fork runs unconditionally: CI + /// supplies `HYPEREVM_RPC_URL` to the shared rainix test workflow from the + /// `RPC_URL_HYPEREVM_FORK` secret, so a missing RPC fails at fork time + /// rather than passing having asserted nothing. RED if the audited 0.1.1 + /// suite is absent from HyperEVM or its in-use beacons are not owned by + /// the HyperEVM token-owner Safe; green otherwise, catching later drift. + function testProdDeployHyperEvmV4() external { + vm.createSelectFork(LibStoxDeployNetworks.HYPEREVM); + checkProd_0_1_1OnChain(); + LibBeaconInvariants.assertProdBeaconsOwnedByChainSafe(block.chainid); + } }