diff --git a/test/script/20260623-upgrade-receipt-vaults-to-v4.t.sol b/test/script/20260623-upgrade-receipt-vaults-to-v4.t.sol index beaf5deb..1bfbe0e1 100644 --- a/test/script/20260623-upgrade-receipt-vaults-to-v4.t.sol +++ b/test/script/20260623-upgrade-receipt-vaults-to-v4.t.sol @@ -13,16 +13,11 @@ import {LibTokenInvariants} from "../../src/lib/LibTokenInvariants.sol"; import {LibAuthoriserInvariants} from "../../src/lib/LibAuthoriserInvariants.sol"; import {LibProdDeployV4} from "../../src/generated/LibProdDeployV4.sol"; import {LibProdDeployV1} from "../../src/lib/LibProdDeployV1.sol"; -import {LibSafeInvariants} from "../../src/lib/LibSafeInvariants.sol"; -import {IGnosisSafe} from "../../src/interface/IGnosisSafe.sol"; -import {IUpgradeableBeacon} from "../../src/lib/LibSafeOps.sol"; import { UpgradeReceiptVaultsToV4, V4ImplementationNotDeployed, - V4CodehashMismatch, - VaultAuthoriserMismatchPostUpgrade + V4CodehashMismatch } from "../../script/20260623-upgrade-receipt-vaults-to-v4.s.sol"; -import {UpgradeReceiptVaultsToV4Harness} from "./UpgradeReceiptVaultsToV4Harness.sol"; /// @title UpgradeReceiptVaultsToV4Test /// @notice Live-fork pin of the vault-authoriser transition executed by @@ -112,80 +107,4 @@ contract UpgradeReceiptVaultsToV4Test is Test { ); upgradeScript.run(); } - - /// @notice Happy path against unmodified live Base state: every - /// pre-flight input is now real (beacon Safe-owned since the 2026-07 - /// migration, audited V4 impl live at `V4_IMPL` with the pinned - /// codehash, V4 authoriser clone deployed + pinned + grant-configured), - /// so `run()` completes end-to-end — pre-flight, bundle build, - /// simulation, post-state, n+1 reversibility — and writes the Safe Tx - /// Builder artifact. This is the exact dry-run the `run-script` - /// dispatch executes to author the signable bundle. - function testRunCompletesAndWritesArtifact() external { - _forkAndMigrateBeaconOwnership(); - assertEq( - V4_IMPL.codehash, - LibProdDeployV4.STOX_RECEIPT_VAULT_CODEHASH_0_1_1, - "live V4 impl codehash != pinned codehash" - ); - UpgradeReceiptVaultsToV4 upgradeScript = new UpgradeReceiptVaultsToV4(); - upgradeScript.run(); - - // The artifact landed with the pinned bundle name and the expected - // shape: 3 beacon upgrades + one setAuthorizer per production vault. - string memory json = vm.readFile("out/v4-upgrade.json"); - assertEq( - vm.parseJsonString(json, ".meta.name"), - "ST0x receipt vault V4 upgrade + authoriser swap", - "artifact bundle name" - ); - uint256 expectedTxCount = 3 + LibTokenInvariants.productionReceiptVaults().length; - assertTrue( - vm.keyExistsJson(json, string.concat(".transactions[", vm.toString(expectedTxCount - 1), "].to")), - "last expected tx present" - ); - assertFalse( - vm.keyExistsJson(json, string.concat(".transactions[", vm.toString(expectedTxCount), "].to")), - "no extra txs" - ); - } - - /// @notice `_assertPostState` reverts `VaultAuthoriserMismatchPostUpgrade` - /// when a production vault still reports a non-V4-clone authoriser after - /// the beacon leg. Drives all three beacons to their V4 impls (so the - /// beacon post-state passes) but leaves authorisers un-swapped, so the - /// per-vault loop trips on vault 0. Exercises the post-state guard the - /// placeholder clone pin otherwise keeps `run()` from ever reaching. - function testAssertPostStateRevertsWhenVaultNotSwapped() external { - _forkAndMigrateBeaconOwnership(); - deployCodeTo("src/concrete/StoxReceiptVault.sol:StoxReceiptVault", V4_IMPL); - deployCodeTo( - "src/concrete/StoxCorporateActionsFacet.sol:StoxCorporateActionsFacet", - LibProdDeployV4.STOX_CORPORATE_ACTIONS_FACET_0_1_1 - ); - // All three V1 beacons to their V4 impls (receipt + wrapped impls - // are live on Base already; the receipt-vault impl was planted - // above). - vm.prank(LibBeaconInvariants.PROD_BEACON_OWNER); - IUpgradeableBeacon(LibProdDeployV1.STOX_RECEIPT_BEACON_V1).upgradeTo(LibProdDeployV4.STOX_RECEIPT_0_1_1); - vm.prank(LibBeaconInvariants.PROD_BEACON_OWNER); - IUpgradeableBeacon(BEACON).upgradeTo(V4_IMPL); - vm.prank(LibBeaconInvariants.PROD_BEACON_OWNER); - IUpgradeableBeacon(LibProdDeployV1.STOX_WRAPPED_TOKEN_VAULT_BEACON_V1) - .upgradeTo(LibProdDeployV4.STOX_WRAPPED_TOKEN_VAULT_0_1_1); - - UpgradeReceiptVaultsToV4Harness harness = new UpgradeReceiptVaultsToV4Harness(); - IGnosisSafe safe = IGnosisSafe(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE); - address[] memory vaults = LibTokenInvariants.productionReceiptVaults(); - address firstAuth = address(IAuthorizableV1(vaults[0]).authorizer()); - vm.expectRevert( - abi.encodeWithSelector( - VaultAuthoriserMismatchPostUpgrade.selector, - vaults[0], - LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE, - firstAuth - ) - ); - harness.callAssertPostState(safe, vaults); - } } diff --git a/test/script/MigrateBeaconOwnersHarness.sol b/test/script/MigrateBeaconOwnersHarness.sol deleted file mode 100644 index 2a37e20f..00000000 --- a/test/script/MigrateBeaconOwnersHarness.sol +++ /dev/null @@ -1,27 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {IGnosisSafe} from "../../src/interface/IGnosisSafe.sol"; -import {LibBeaconInvariants} from "../../src/lib/LibBeaconInvariants.sol"; -import {LibSafeOps} from "../../src/lib/LibSafeOps.sol"; - -/// @title MigrateBeaconOwnersHarness -/// @notice External-call shim around the migration steps so `vm.expectRevert` -/// can intercept the typed errors raised by `LibSafeInvariants`. Library -/// `internal` functions inline into the test and would fail the -/// `expectRevert` depth check otherwise. The harness mirrors the exact -/// sequence `MigrateBeaconOwners.run()` performs, minus the `vm.broadcast` -/// wrapper (the test drives the ownership transfer via `vm.prank(EOA)` to -/// simulate the on-chain broadcast's effect). -contract MigrateBeaconOwnersHarness { - function callAssertBeaconInvariants(address beacon, address expectedOwner, address expectedImpl) external view { - LibBeaconInvariants.assertBeaconInvariants(beacon, expectedOwner, expectedImpl); - } - - function callSimulateBeaconNPlus1(IGnosisSafe safe, address beacon, address currentImpl, uint256 threshold) - external - { - LibSafeOps.simulateBeaconNPlus1(safe, beacon, currentImpl, threshold); - } -} diff --git a/test/script/MigrateBeaconOwnersTest.t.sol b/test/script/MigrateBeaconOwnersTest.t.sol deleted file mode 100644 index ed5fc191..00000000 --- a/test/script/MigrateBeaconOwnersTest.t.sol +++ /dev/null @@ -1,185 +0,0 @@ -// 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 {Ownable} from "@openzeppelin-contracts-5.6.1/access/Ownable.sol"; -import {IBeacon} from "@openzeppelin-contracts-5.6.1/proxy/beacon/IBeacon.sol"; - -import {IGnosisSafe} from "../../src/interface/IGnosisSafe.sol"; -import {LibProdDeployV1} from "../../src/lib/LibProdDeployV1.sol"; -import {LibSafeInvariants} from "../../src/lib/LibSafeInvariants.sol"; -import {LibBeaconInvariants, BeaconOwnerMismatch} from "../../src/lib/LibBeaconInvariants.sol"; -import {MigrateBeaconOwnersHarness} from "./MigrateBeaconOwnersHarness.sol"; -import {LibRainDeploy} from "rain-deploy-0.1.4/src/lib/LibRainDeploy.sol"; - -/// @title MigrateBeaconOwnersTest -/// @notice End-to-end fork tests for the beacon-ownership migration -/// (EXECUTED on Base, 2026-07 — every V1 beacon is now Safe-owned live). -/// The migration walk is still exercised in full: the pre-migration -/// EOA-owned state is reconstructed deterministically via `vm.store` on the -/// beacons' OZ `Ownable` owner slot (slot 0), so the coverage no longer -/// depends on the live chain being in the pre-execution state. The live -/// post-state (Safe-owned, real transfers) is asserted by -/// `testLivePostStateSafeOwned` and by the `BeaconOwnerMigrationPinTest` -/// migration-window cron invariant. -/// @dev Uses an unpinned Base head fork (same precedent as -/// `MigrateMultisigThresholdTest`): any drift in the live beacon state -/// surfaces on the next CI run rather than being frozen against a stale -/// snapshot. -contract MigrateBeaconOwnersTest is Test { - /// @notice OZ `Ownable` stores `_owner` in slot 0 on the - /// `UpgradeableBeacon`. - bytes32 internal constant OWNABLE_OWNER_SLOT = bytes32(0); - - /// @notice Live Safe handle, reset per fork. - IGnosisSafe internal safe; - - /// @notice The harness deployed fresh per fork. - MigrateBeaconOwnersHarness internal harness; - - /// @notice The three beacons under migration, in the script's order. - address[3] internal beaconList = [ - LibProdDeployV1.STOX_RECEIPT_BEACON_V1, - LibProdDeployV1.STOX_RECEIPT_VAULT_BEACON_V1, - LibProdDeployV1.STOX_WRAPPED_TOKEN_VAULT_BEACON_V1 - ]; - - /// @notice Each beacon's pinned current implementation, index-aligned with - /// `beaconList`. - address[3] internal implList = [ - LibProdDeployV1.STOX_RECEIPT_IMPLEMENTATION, - LibProdDeployV1.STOX_RECEIPT_VAULT_IMPLEMENTATION, - LibProdDeployV1.STOX_WRAPPED_TOKEN_VAULT_IMPLEMENTATION - ]; - - function selectBaseFork() internal { - vm.createSelectFork(LibRainDeploy.BASE); - safe = IGnosisSafe(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE); - harness = new MigrateBeaconOwnersHarness(); - } - - /// @notice Reconstruct the pre-migration state on the fork: write the - /// rainlang.eth EOA back into each beacon's `Ownable` owner slot. The - /// migration executed on Base in 2026-07, so the live fork starts - /// Safe-owned; the walk tests rewind ownership deterministically - /// instead of depending on live pre-execution state. - function rewindToEoaOwned() internal { - for (uint256 i = 0; i < beaconList.length; i++) { - vm.store(beaconList[i], OWNABLE_OWNER_SLOT, bytes32(uint256(uint160(LibProdDeployV1.BEACON_INITIAL_OWNER)))); - assertEq(Ownable(beaconList[i]).owner(), LibProdDeployV1.BEACON_INITIAL_OWNER, "rewind failed: owner slot"); - } - } - - /// @notice Simulate the migration's on-chain effect: prank the EOA owner - /// and transfer each beacon to the Safe. Models exactly what - /// `MigrateBeaconOwners.run()`'s broadcast block did. - function simulateTransfers() internal { - for (uint256 i = 0; i < beaconList.length; i++) { - vm.prank(LibProdDeployV1.BEACON_INITIAL_OWNER); - Ownable(beaconList[i]).transferOwnership(LibBeaconInvariants.PROD_BEACON_OWNER); - } - } - - /// @notice LIVE post-state: every beacon on Base head is Safe-owned with - /// its implementation unchanged — the executed migration's outcome, - /// asserted against the real chain with no state reconstruction. - function testLivePostStateSafeOwned() external { - selectBaseFork(); - for (uint256 i = 0; i < beaconList.length; i++) { - harness.callAssertBeaconInvariants(beaconList[i], LibBeaconInvariants.PROD_BEACON_OWNER, implList[i]); - assertEq(Ownable(beaconList[i]).owner(), LibBeaconInvariants.PROD_BEACON_OWNER, "beacon Safe-owned"); - assertEq(IBeacon(beaconList[i]).implementation(), implList[i], "implementation unchanged by migration"); - } - } - - /// @notice Pre-flight passes against the (reconstructed) EOA-owned state - /// for all three beacons. This is the gate `run()` ran before - /// broadcasting. - function testPreflightPassesAgainstEoaOwnedState() external { - selectBaseFork(); - rewindToEoaOwned(); - for (uint256 i = 0; i < beaconList.length; i++) { - // No revert == invariant holds. - harness.callAssertBeaconInvariants(beaconList[i], LibProdDeployV1.BEACON_INITIAL_OWNER, implList[i]); - } - } - - /// @notice Full migration walk: pre-flight (EOA) passes, transfers - /// applied, post-state (Safe) passes, n+1 reversibility passes for every - /// beacon. This is the happy-path mirror of `MigrateBeaconOwners.run()`, - /// replayed from the reconstructed pre-state. - function testFullMigrationWalk() external { - selectBaseFork(); - rewindToEoaOwned(); - - // Pre-flight: every beacon EOA-owned. - for (uint256 i = 0; i < beaconList.length; i++) { - harness.callAssertBeaconInvariants(beaconList[i], LibProdDeployV1.BEACON_INITIAL_OWNER, implList[i]); - } - - // Simulate the broadcast effect. - simulateTransfers(); - - // Post-state: every beacon now Safe-owned, implementations unchanged. - for (uint256 i = 0; i < beaconList.length; i++) { - harness.callAssertBeaconInvariants(beaconList[i], LibBeaconInvariants.PROD_BEACON_OWNER, implList[i]); - assertEq(Ownable(beaconList[i]).owner(), LibBeaconInvariants.PROD_BEACON_OWNER, "beacon now Safe-owned"); - assertEq(IBeacon(beaconList[i]).implementation(), implList[i], "implementation unchanged by transfer"); - } - - // n+1 reversibility: the Safe can act on each beacon via an idempotent - // upgradeTo routed through execTransaction. The post-condition inside - // the helper asserts the implementation is preserved. - for (uint256 i = 0; i < beaconList.length; i++) { - harness.callSimulateBeaconNPlus1( - safe, beaconList[i], implList[i], LibSafeInvariants.STOX_TOKEN_OWNER_SAFE_THRESHOLD - ); - // After the idempotent n+1, the beacon still points at the same - // implementation and is still Safe-owned. - assertEq(IBeacon(beaconList[i]).implementation(), implList[i], "implementation preserved through n+1"); - assertEq( - Ownable(beaconList[i]).owner(), LibBeaconInvariants.PROD_BEACON_OWNER, "still Safe-owned after n+1" - ); - } - } - - /// @notice Inverted: the pre-flight rejects a wrong expected owner. From - /// the reconstructed EOA-owned state, asserting the beacon should be - /// Safe-owned trips `BeaconOwnerMismatch` with the actual EOA owner. - /// This is the property that makes the post-state assertion meaningful — - /// it would catch a transfer that silently failed. - function testInvertedWrongExpectedOwnerReverts() external { - selectBaseFork(); - rewindToEoaOwned(); - address beacon = beaconList[0]; - vm.expectRevert( - abi.encodeWithSelector( - BeaconOwnerMismatch.selector, - beacon, - LibBeaconInvariants.PROD_BEACON_OWNER, - LibProdDeployV1.BEACON_INITIAL_OWNER - ) - ); - harness.callAssertBeaconInvariants(beacon, LibBeaconInvariants.PROD_BEACON_OWNER, implList[0]); - } - - /// @notice Inverted: with the migration landed (live state), asserting - /// the OLD EOA owner trips `BeaconOwnerMismatch` reporting the Safe as - /// the actual owner. Confirms the post-state assertion is sensitive to - /// the ownership flip in both directions — and doubles as the dispatch - /// gate: a re-run of the script's pre-flight now reverts here. - function testInvertedStaleEoaOwnerRevertsPostTransfer() external { - selectBaseFork(); - address beacon = beaconList[0]; - vm.expectRevert( - abi.encodeWithSelector( - BeaconOwnerMismatch.selector, - beacon, - LibProdDeployV1.BEACON_INITIAL_OWNER, - LibBeaconInvariants.PROD_BEACON_OWNER - ) - ); - harness.callAssertBeaconInvariants(beacon, LibProdDeployV1.BEACON_INITIAL_OWNER, implList[0]); - } -} diff --git a/test/script/UpgradeReceiptVaultsToV4Harness.sol b/test/script/UpgradeReceiptVaultsToV4Harness.sol deleted file mode 100644 index cd12e134..00000000 --- a/test/script/UpgradeReceiptVaultsToV4Harness.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: LicenseRef-DCL-1.0 -// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd -pragma solidity =0.8.25; - -import {IGnosisSafe} from "../../src/interface/IGnosisSafe.sol"; -import {UpgradeReceiptVaultsToV4} from "../../script/20260623-upgrade-receipt-vaults-to-v4.s.sol"; - -/// @title UpgradeReceiptVaultsToV4Harness -/// @notice Subclass of the upgrade script that exposes its `internal` -/// post-state assertion as `external` so `vm.expectRevert` can intercept the -/// typed `VaultAuthoriserMismatchPostUpgrade` it raises. The pre-flight guards -/// are exercised via `run()` directly in the tests (they revert before any -/// bundle is built); only the post-state — which `run()` reaches only after -/// the still-placeholder clone pin is hydrated — needs this seam to be driven -/// against a deliberately-malformed (un-swapped) state. -contract UpgradeReceiptVaultsToV4Harness is UpgradeReceiptVaultsToV4 { - function callAssertPostState(IGnosisSafe safe, address[] memory vaults) external view { - _assertPostState(safe, vaults); - } -} diff --git a/test/src/concrete/upgrade/V3UpgradeShadowFork.t.sol b/test/src/concrete/upgrade/V3UpgradeShadowFork.t.sol index 4a82ad1b..d58c07f7 100644 --- a/test/src/concrete/upgrade/V3UpgradeShadowFork.t.sol +++ b/test/src/concrete/upgrade/V3UpgradeShadowFork.t.sol @@ -7,23 +7,12 @@ import {Ownable} from "@openzeppelin-contracts-5.6.1/access/Ownable.sol"; import {IBeacon} from "@openzeppelin-contracts-5.6.1/proxy/beacon/IBeacon.sol"; import {IERC20Metadata} from "@openzeppelin-contracts-5.6.1/token/ERC20/extensions/IERC20Metadata.sol"; -import {IAccessControl} from "@openzeppelin-contracts-5.6.1/access/IAccessControl.sol"; import {LibProdDeployV1} from "../../../../src/lib/LibProdDeployV1.sol"; import {LibProdDeployV4} from "../../../../src/generated/LibProdDeployV4.sol"; import {LibBeaconInvariants} from "../../../../src/lib/LibBeaconInvariants.sol"; import {LibSafeInvariants} from "../../../../src/lib/LibSafeInvariants.sol"; -import {LibAuthoriserInvariants} from "../../../../src/lib/LibAuthoriserInvariants.sol"; import {LibTokenInvariants} from "../../../../src/lib/LibTokenInvariants.sol"; import {LibSafeOps, IUpgradeableBeacon} from "../../../../src/lib/LibSafeOps.sol"; -import {SCHEDULE_CORPORATE_ACTION} from "../../../../src/lib/LibCorporateAction.sol"; -import { - StoxOffchainAssetReceiptVaultAuthorizerV1 -} from "../../../../src/concrete/authorize/StoxOffchainAssetReceiptVaultAuthorizerV1.sol"; -import { - OffchainAssetReceiptVaultAuthorizerV1Config -} from "rain-vats-0.1.6/src/concrete/authorize/OffchainAssetReceiptVaultAuthorizerV1.sol"; -import {ICloneableFactoryV2} from "rain-factory-0.1.1/src/interface/ICloneableFactoryV2.sol"; -import {LibCloneFactoryDeploy} from "rain-factory-0.1.1/src/lib/LibCloneFactoryDeploy.sol"; import { ICorporateActionsV1, ACTION_TYPE_STOCK_SPLIT_V1, @@ -34,7 +23,7 @@ import {LibRainDeploy} from "rain-deploy-0.1.4/src/lib/LibRainDeploy.sol"; import {IReceiptVaultV3} from "rain-vats-0.1.6/src/interface/IReceiptVaultV3.sol"; import {IReceiptV3} from "rain-vats-0.1.6/src/interface/IReceiptV3.sol"; import {IAuthorizableV1} from "rain-vats-0.1.6/src/interface/IAuthorizableV1.sol"; -import {IAuthorizeV1, Unauthorized} from "rain-vats-0.1.6/src/interface/IAuthorizeV1.sol"; +import {IAuthorizeV1} from "rain-vats-0.1.6/src/interface/IAuthorizeV1.sol"; import {ICertifiableV1} from "rain-vats-0.1.6/src/interface/ICertifiableV1.sol"; import {ERC1967_BEACON_SLOT} from "rain-extrospection-0.1.1/src/lib/LibExtrospectERC1967BeaconProxy.sol"; @@ -226,57 +215,6 @@ contract V3UpgradeShadowForkTest is Test { ); } - /// @notice The OTHER half of the migration bundle — the `setAuthorizer` - /// swap — actually re-wires authorisation on a LIVE receipt vault. The - /// tests above cover the beacon (impl) leg; this covers the authoriser - /// leg: deploy a fresh V4 corporate-action-aware authoriser clone, - /// `setAuthorizer` the live vault onto it as the Safe owner, and prove the - /// vault now gates through the new clone — a granted `SCHEDULE_CORPORATE_ - /// ACTION` caller is permitted, an ungranted caller is rejected with the - /// exact `Unauthorized` error. The corporate-action permission is the V4 - /// delta: the pre-swap production authoriser configures no role admin for - /// it, so only the swapped V4 clone can gate it. - function testAuthoriserSwapReWiresGatingOnLiveVault() external { - // Pre-swap: the live vault reports the current production authoriser. - assertEq( - address(IAuthorizableV1(LIVE_RECEIPT_VAULT).authorizer()), - LibAuthoriserInvariants.STOX_PROD_AUTHORISER, - "pre-swap authoriser is the live production authoriser" - ); - - // Deploy a fresh V4 authoriser clone. The impl `_disableInitializers` - // in its constructor, so it must be cloned + initialised via the - // CloneFactory — the same path the production clone-deploy broadcast - // uses. - address cloneAdmin = makeAddr("cloneAdmin"); - StoxOffchainAssetReceiptVaultAuthorizerV1 impl = new StoxOffchainAssetReceiptVaultAuthorizerV1(); - address clone = ICloneableFactoryV2(LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_ADDRESS) - .clone(address(impl), abi.encode(OffchainAssetReceiptVaultAuthorizerV1Config({initialAdmin: cloneAdmin}))); - - // Grant the corporate-action scheduling role to one user. `cloneAdmin` - // holds `SCHEDULE_CORPORATE_ACTION_ADMIN` from init (the V4 extension), - // so it is the role admin able to grant `SCHEDULE_CORPORATE_ACTION`. - address scheduler = makeAddr("scheduler"); - address outsider = makeAddr("outsider"); - vm.prank(cloneAdmin); - IAccessControl(clone).grantRole(SCHEDULE_CORPORATE_ACTION, scheduler); - - // Swap: the Safe (vault owner) rewires the live vault onto the clone. - vm.prank(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE); - ISetAuthorizer(LIVE_RECEIPT_VAULT).setAuthorizer(IAuthorizeV1(clone)); - - // The swap landed: the vault now routes authorisation through the clone. - IAuthorizeV1 wired = IAuthorizableV1(LIVE_RECEIPT_VAULT).authorizer(); - assertEq(address(wired), clone, "post-swap authoriser is the new V4 clone"); - - // The swapped authoriser gates corporate actions: the granted - // scheduler is permitted (no revert)... - wired.authorize(scheduler, SCHEDULE_CORPORATE_ACTION, ""); - // ...and an ungranted caller is rejected with the exact typed error. - vm.expectRevert(abi.encodeWithSelector(Unauthorized.selector, outsider, SCHEDULE_CORPORATE_ACTION, bytes(""))); - wired.authorize(outsider, SCHEDULE_CORPORATE_ACTION, ""); - } - // ------------------------------------------------------------------------- // TODO(audit): enumerate v0.1.1 findings — pending report from Josh/DM. // @@ -290,10 +228,3 @@ contract V3UpgradeShadowForkTest is Test { // absence of this section's tests as evidence the findings are fixed. // ------------------------------------------------------------------------- } - -/// @dev Local mirror of the receipt-vault `setAuthorizer(IAuthorizeV1)` -/// owner-gated selector. Avoids dragging the full `OffchainAssetReceiptVault` -/// storage inheritance into this test just to encode one call. -interface ISetAuthorizer { - function setAuthorizer(IAuthorizeV1 newAuthorizer) external; -} diff --git a/test/src/lib/LibInvariants.t.sol b/test/src/lib/LibInvariants.t.sol index d0c42cf5..5462e412 100644 --- a/test/src/lib/LibInvariants.t.sol +++ b/test/src/lib/LibInvariants.t.sol @@ -8,7 +8,7 @@ import {IGnosisSafe} from "../../../src/interface/IGnosisSafe.sol"; import {LibInvariants} from "../../../src/lib/LibInvariants.sol"; import {LibSafeInvariants} from "../../../src/lib/LibSafeInvariants.sol"; import {LibTokenInvariants} from "../../../src/lib/LibTokenInvariants.sol"; -import {LibAuthoriserInvariants} from "../../../src/lib/LibAuthoriserInvariants.sol"; +import {LibProdDeployV4} from "../../../src/generated/LibProdDeployV4.sol"; /// @title LibInvariantsTest /// @notice Exercises the multichain production-state orchestrator. The Base @@ -28,10 +28,11 @@ contract LibInvariantsTest is Test { // Both forms must return silently. `assertProductionState` asserts the // shared token-owner Safe + shared grant map, so only the token table - // and the live authoriser are passed. + // and the live authoriser are passed. The live authoriser is the V4 + // clone since the swap batch executed on Base (2026-07). LibInvariants.assertAll(safe); LibInvariants.assertProductionState( - LibTokenInvariants.productionTokensBase(), LibAuthoriserInvariants.STOX_PROD_AUTHORISER + LibTokenInvariants.productionTokensBase(), LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE ); } } diff --git a/test/src/lib/LibTokenInvariants.t.sol b/test/src/lib/LibTokenInvariants.t.sol index d8bff685..58d63273 100644 --- a/test/src/lib/LibTokenInvariants.t.sol +++ b/test/src/lib/LibTokenInvariants.t.sol @@ -3,7 +3,7 @@ pragma solidity =0.8.25; import {Test} from "forge-std-1.16.1/src/Test.sol"; -import {LibAuthoriserInvariants} from "../../../src/lib/LibAuthoriserInvariants.sol"; +import {LibProdDeployV4} from "../../../src/generated/LibProdDeployV4.sol"; import {LibTokenInvariants, IOwnable, ReceiptVaultOwnerMismatch} from "../../../src/lib/LibTokenInvariants.sol"; import {LibSafeInvariants} from "../../../src/lib/LibSafeInvariants.sol"; import {LibTokenInvariantsHarness} from "./LibTokenInvariantsHarness.sol"; @@ -15,10 +15,10 @@ import {LibRainDeploy} from "rain-deploy-0.1.4/src/lib/LibRainDeploy.sol"; /// `authorizer()`. /// /// Both uniformity invariants currently hold on-chain (every vault is -/// owned by `LibSafeInvariants.STOX_TOKEN_OWNER_SAFE` and reports the pinned -/// `LibAuthoriserInvariants.STOX_PROD_AUTHORISER`), so the positive -/// cases pass against the live Base fork. The inverted ownership-drift -/// case is also exercised here for full error-path coverage. +/// owned by `LibSafeInvariants.STOX_TOKEN_OWNER_SAFE` and reports the V4 +/// authoriser clone `LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE`), so the +/// positive cases pass against the live Base fork. The inverted +/// ownership-drift case is also exercised here for full error-path coverage. /// @dev Uses an unpinned Base head fork (same precedent as the other /// prod-state drift detectors in this repo), so the next CI run reflects the /// current on-chain wiring. Pinning would freeze the invariant assertions @@ -43,12 +43,17 @@ contract LibTokenInvariantsTest is Test { LibTokenInvariants.assertUniformOwnership(LibSafeInvariants.STOX_TOKEN_OWNER_SAFE); } - /// @notice Every production receipt vault reports - /// `LibAuthoriserInvariants.STOX_PROD_AUTHORISER`. Passes against - /// the live chain state: vault authoriser is uniform. + /// @notice Every production receipt vault reports the V4 authoriser clone + /// (`LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE`) — the production + /// authoriser since the V4 swap batch executed on Base (2026-07). + /// Passes against the live chain state: vault authoriser is uniform. + /// Deliberately STRICT (not migration-window gated): when a newly + /// deployed token enters the table still wired to a previous authoriser, + /// this goes red until its swap batch executes — the forcing function + /// that keeps the table and the live wiring in lock-step. function testProdReceiptVaultsShareUniformAuthoriser() external { selectBaseFork(); - LibTokenInvariants.assertUniformAuthoriser(LibAuthoriserInvariants.STOX_PROD_AUTHORISER); + LibTokenInvariants.assertUniformAuthoriser(LibProdDeployV4.STOX_PROD_AUTHORISER_V4_CLONE); } /// @notice Token-side ownership drift trips `ReceiptVaultOwnerMismatch`.