Files
self/contracts/test/foundry/UpgradeToAccessControl.t.sol
Evi Nova bc4e52bb1e Refactor/multitiered multisig roles (#1483)
* refactor: switch to multitiered governance with multisigs

* feat: add scripts for assisting with upgrading contracts and

* test: add tests for governance upgrade

* chore: install Foundry with Hardhat compatability

* fix: add separate intializeGovernance function for upgrading

Uses reinitializer modifier for proper security around function call

* feat: migrate new function to AccessControl governance

* test: full end to end upgrade typescript test

* chore: add hardhat-upgrade

* chore: add foundry outputs to gitignore

* test: add Foundry upgrade script and test for deployed contracts

* refactor: update PCR0 inputs to be 32 bytes for GCP image hashes

Still pad to 48 bytes to ensure compatibility with mobile app.

* feat: add PCR0 migration script + test file

* fix: use custom natspec to prevent constructor warnings on upgrade

* test: cleanup tests and add role transfer to upgrade script

* test: add deployed libraries to foundry.toml for proper library linking

* chore: add /contracts/broadcast to gitignore for foundry deployments

* fix: set variable in initializer instead of defining in declaration

* test: improve upgrade test script to check all state variables

* docs: better explain safety behind using unsafeSkipStorageCheck

* doc: add guide for upgrading to AccessControl governance

* style: change multisig role names

CRITICAL_ROLE -> SECURITY_ROLE (3/5)
STANDARD_ROLE -> OPERATIONRS_ROLE (2/5)

* refactor: change OFAC + CSCA root update functions to 2/5 multisig

* fix: package version clashes + outdated code from old ver of packages

OpenZeppelin v5.5.0 no longer requires __UUPS_Upgradeable_Init, new OZ version requires opcodes that need cancun evmVersion, hard defining @noble/hashes led to clashes with other dependencies

* fix: fix PCR0 tests broken from change in byte size

* feat: add contract upgrade tooling with Safe multisig integration

- Add unified 'upgrade' Hardhat task with automatic safety checks
- Add deployment registry for version tracking
- Add Safe SDK integration for auto-proposing upgrades
- Update UPGRADE_GUIDE.md with new workflow documentation
- Validate version increments, reinitializer, and storage layout

* fix: revert fix on Hub V1 contract that is not supported

* style: update upgraded contracts to not use custom:version-history

* fix: V1 test requires old style as well

* fix: correct registry currentVersion to reflect actual deployed versions

On-chain verification confirmed all contracts are using OLD Ownable2StepUpgradeable:
- Hub: 2.11.0 (was incorrectly 2.12.0)
- Registry: 1.1.0 (was incorrectly 1.2.0)
- IdCard: 1.1.0 (was incorrectly 1.2.0)
- Aadhaar: 1.1.0 (was incorrectly 1.2.0)

Owner address: 0xcaee7aaf115f04d836e2d362a7c07f04db436bd0

* fix: upgrade script now correctly handles pre-defined versions in registry

When upgrading to a version that already exists in registry.json (like 2.12.0),
the script now uses that version's initializerVersion instead of incrementing
from the latest version. This fixes the reinitializer validation for the
governance upgrade.

* fix: upgrade script handles Ownable contracts and outputs transaction data

- Detect Ownable pattern before creating Safe proposals
- Output transaction data for owner direct execution in --prepare-only mode
- Use initializerFunction from registry (initializeGovernance) instead of constructing names
- Skip Safe proposal creation for initial Ownable → AccessControl upgrade
- After upgrade, owner grants SECURITY_ROLE to Safe for future upgrades

* feat: IdentityVerificationHub v2.12.0 deployed on Celo

- Implementation: 0x05FB9D7830889cc389E88198f6A224eA87F01151
- Changelog: Governance upgrade

* feat: IdentityRegistryIdCard v1.2.0 deployed on Celo

- Implementation: 0x7d5e4b7D4c3029aF134D50642674Af8F875118a4
- Changelog: Governance upgrade

* feat: IdentityRegistryAadhaar v1.2.0 deployed on Celo

- Implementation: 0xbD861A9cecf7B0A9631029d55A8CE1155e50697c
- Changelog: Governance upgrade

* feat: IdentityRegistry v1.2.0 deployed on Celo

- Implementation: 0x81E7F74560FAF7eE8DE3a36A5a68B6cbc429Cd36
- Changelog: Governance upgrade

* feat: add multisig addresses to registry

* feat: PCR0Manager v1.2.0 deployed on Celo

- Implementation: 0x9743fe2C1c3D2b068c56dE314e9B10DA9c904717
- Changelog: Governance upgrade

* refactor: cleanup old scripts

* chore: yarn prettier formatting
2025-12-10 17:30:50 +10:00

356 lines
16 KiB
Solidity

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;
import {Test} from "forge-std/Test.sol";
import {console2} from "forge-std/console2.sol";
import {Upgrades, Options} from "openzeppelin-foundry-upgrades/Upgrades.sol";
import {IdentityVerificationHubImplV2} from "../../contracts/IdentityVerificationHubImplV2.sol";
import {IdentityRegistryImplV1} from "../../contracts/registry/IdentityRegistryImplV1.sol";
import {IdentityRegistryIdCardImplV1} from "../../contracts/registry/IdentityRegistryIdCardImplV1.sol";
import {IdentityRegistryAadhaarImplV1} from "../../contracts/registry/IdentityRegistryAadhaarImplV1.sol";
import {Ownable2StepUpgradeable} from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
/**
* @title UpgradeToAccessControlTest
* @notice Fork test for upgrading contracts from Ownable to AccessControl
*
* This test:
* 1. Forks Celo mainnet at current block
* 2. Captures pre-upgrade state from real deployed contracts
* 3. Executes upgrades to AccessControl governance
* 4. Verifies ALL state is preserved (no storage collisions)
* 5. Tests governance functionality
* 6. Simulates role transfer to multisigs
* 7. Verifies deployer has no control after transfer
*
* Run with:
* forge test --match-contract UpgradeToAccessControlTest --fork-url $CELO_RPC_URL -vvv
*/
contract UpgradeToAccessControlTest is Test {
// ============================================================================
// DEPLOYED CONTRACT ADDRESSES (Celo Mainnet)
// ============================================================================
address constant HUB_PROXY = 0xe57F4773bd9c9d8b6Cd70431117d353298B9f5BF;
address constant REGISTRY_PASSPORT_PROXY = 0x37F5CB8cB1f6B00aa768D8aA99F1A9289802A968;
address constant REGISTRY_ID_CARD_PROXY = 0xeAD1E6Ec29c1f3D33a0662f253a3a94D189566E1;
address constant REGISTRY_AADHAAR_PROXY = 0xd603Fa8C8f4694E8DD1DcE1f27C0C3fc91e32Ac4;
address constant CUSTOM_VERIFIER = 0x9E66B82Da87309fAE1403078d498a069A30860c4;
address constant POSEIDON_T3 = 0xF134707a4C4a3a76b8410fC0294d620A7c341581;
// Test accounts
address deployer;
address securityMultisig;
address operationsMultisig;
// Contracts
IdentityVerificationHubImplV2 hub;
IdentityRegistryImplV1 passportRegistry;
IdentityRegistryIdCardImplV1 idCardRegistry;
IdentityRegistryAadhaarImplV1 aadhaarRegistry;
// Governance roles
bytes32 public constant SECURITY_ROLE = keccak256("SECURITY_ROLE");
bytes32 public constant OPERATIONS_ROLE = keccak256("OPERATIONS_ROLE");
// Pre-upgrade state - captures ALL publicly accessible critical state variables
struct PreUpgradeState {
// Hub state (4 variables)
address hubRegistryPassport;
address hubRegistryIdCard;
address hubRegistryAadhaar;
uint256 hubAadhaarWindow;
// Passport Registry state (6 variables)
uint256 passportIdentityRoot;
uint256 passportDscKeyRoot;
uint256 passportPassportNoOfacRoot;
uint256 passportNameDobOfacRoot;
uint256 passportNameYobOfacRoot;
uint256 passportCscaRoot;
// ID Card Registry state (5 variables)
uint256 idCardIdentityRoot;
uint256 idCardDscKeyRoot;
uint256 idCardNameDobOfacRoot;
uint256 idCardNameYobOfacRoot;
uint256 idCardCscaRoot;
// Aadhaar Registry state (3 variables)
uint256 aadhaarIdentityRoot;
uint256 aadhaarNameDobOfacRoot;
uint256 aadhaarNameYobOfacRoot;
}
PreUpgradeState preState;
function setUp() public {
console2.log("================================================================================");
console2.log("CELO MAINNET FORK TEST: Ownable -> AccessControl Upgrade");
console2.log("================================================================================");
// Initialize contract references to get current owner
hub = IdentityVerificationHubImplV2(HUB_PROXY);
passportRegistry = IdentityRegistryImplV1(REGISTRY_PASSPORT_PROXY);
idCardRegistry = IdentityRegistryIdCardImplV1(REGISTRY_ID_CARD_PROXY);
aadhaarRegistry = IdentityRegistryAadhaarImplV1(REGISTRY_AADHAAR_PROXY);
// Get the actual current owner from the deployed contracts
deployer = Ownable2StepUpgradeable(address(hub)).owner();
// Set up multisig accounts for testing role transfer
securityMultisig = makeAddr("securityMultisig");
operationsMultisig = makeAddr("operationsMultisig");
vm.deal(deployer, 100 ether);
console2.log("Current Owner (will execute upgrade):", deployer);
console2.log("Critical Multisig (will receive roles):", securityMultisig);
console2.log("Standard Multisig (will receive roles):", operationsMultisig);
}
function testFullUpgradeWorkflow() public {
console2.log("\n=== Phase 1: Capture Pre-Upgrade State (ALL Accessible State Variables) ===");
// Hub state (4 variables)
preState.hubRegistryPassport = hub.registry(bytes32("e_passport"));
preState.hubRegistryIdCard = hub.registry(bytes32("eu_id_card"));
preState.hubRegistryAadhaar = hub.registry(bytes32("aadhaar"));
preState.hubAadhaarWindow = hub.AADHAAR_REGISTRATION_WINDOW();
// Passport Registry state (6 variables)
preState.passportIdentityRoot = passportRegistry.getIdentityCommitmentMerkleRoot();
preState.passportDscKeyRoot = passportRegistry.getDscKeyCommitmentMerkleRoot();
preState.passportPassportNoOfacRoot = passportRegistry.getPassportNoOfacRoot();
preState.passportNameDobOfacRoot = passportRegistry.getNameAndDobOfacRoot();
preState.passportNameYobOfacRoot = passportRegistry.getNameAndYobOfacRoot();
preState.passportCscaRoot = passportRegistry.getCscaRoot();
// ID Card Registry state (5 variables)
preState.idCardIdentityRoot = idCardRegistry.getIdentityCommitmentMerkleRoot();
preState.idCardDscKeyRoot = idCardRegistry.getDscKeyCommitmentMerkleRoot();
preState.idCardNameDobOfacRoot = idCardRegistry.getNameAndDobOfacRoot();
preState.idCardNameYobOfacRoot = idCardRegistry.getNameAndYobOfacRoot();
preState.idCardCscaRoot = idCardRegistry.getCscaRoot();
// Aadhaar Registry state (3 variables)
preState.aadhaarIdentityRoot = aadhaarRegistry.getIdentityCommitmentMerkleRoot();
preState.aadhaarNameDobOfacRoot = aadhaarRegistry.getNameAndDobOfacRoot();
preState.aadhaarNameYobOfacRoot = aadhaarRegistry.getNameAndYobOfacRoot();
console2.log("Captured Hub state: 4 variables");
console2.log("Captured Passport Registry state: 6 variables");
console2.log("Captured ID Card Registry state: 5 variables");
console2.log("Captured Aadhaar Registry state: 3 variables");
console2.log("Total state variables captured: 18");
console2.log("\n=== Phase 2: Execute Upgrades ===");
vm.startPrank(deployer);
// Upgrade Hub
console2.log("Upgrading Hub...");
Options memory hubOpts;
// Skip ALL OpenZeppelin checks because:
// 1. We're changing base contracts (Ownable->AccessControl) which confuses the validator
// 2. ERC-7201 namespaced storage prevents any collision
// 3. We COMPREHENSIVELY verify safety in this test:
// - Phase 3: State preservation (no data loss)
// - Phase 3.5: Library linkage (same addresses)
// - Phase 4-6: Governance functionality (roles work correctly)
hubOpts.unsafeSkipAllChecks = true;
Upgrades.upgradeProxy(
HUB_PROXY,
"IdentityVerificationHubImplV2.sol",
abi.encodeCall(IdentityVerificationHubImplV2.initializeGovernance, ()),
hubOpts
);
// Upgrade Passport Registry
console2.log("Upgrading Passport Registry...");
Options memory passportOpts;
passportOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
Upgrades.upgradeProxy(
REGISTRY_PASSPORT_PROXY,
"IdentityRegistryImplV1.sol:IdentityRegistryImplV1",
abi.encodeCall(IdentityRegistryImplV1.initializeGovernance, ()),
passportOpts
);
// Upgrade ID Card Registry
console2.log("Upgrading ID Card Registry...");
Options memory idCardOpts;
idCardOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
Upgrades.upgradeProxy(
REGISTRY_ID_CARD_PROXY,
"IdentityRegistryIdCardImplV1.sol:IdentityRegistryIdCardImplV1",
abi.encodeCall(IdentityRegistryIdCardImplV1.initializeGovernance, ()),
idCardOpts
);
// Upgrade Aadhaar Registry
console2.log("Upgrading Aadhaar Registry...");
Options memory aadhaarOpts;
aadhaarOpts.unsafeSkipAllChecks = true; // Safe: verified in test phases 3-6
Upgrades.upgradeProxy(
REGISTRY_AADHAAR_PROXY,
"IdentityRegistryAadhaarImplV1.sol:IdentityRegistryAadhaarImplV1",
abi.encodeCall(IdentityRegistryAadhaarImplV1.initializeGovernance, ()),
aadhaarOpts
);
vm.stopPrank();
console2.log("All upgrades completed");
console2.log("\n=== Phase 3: Verify State Preservation (ALL 18 State Variables) ===");
// Hub state verification (4 variables)
assertEq(hub.registry(bytes32("e_passport")), preState.hubRegistryPassport, "Hub passport registry changed");
assertEq(hub.registry(bytes32("eu_id_card")), preState.hubRegistryIdCard, "Hub ID card registry changed");
assertEq(hub.registry(bytes32("aadhaar")), preState.hubRegistryAadhaar, "Hub aadhaar registry changed");
assertEq(hub.AADHAAR_REGISTRATION_WINDOW(), preState.hubAadhaarWindow, "Hub aadhaar window changed");
console2.log("Hub state: 4/4 variables preserved");
// Passport Registry state verification (6 variables)
assertEq(
passportRegistry.getIdentityCommitmentMerkleRoot(),
preState.passportIdentityRoot,
"Passport identity root changed"
);
assertEq(
passportRegistry.getDscKeyCommitmentMerkleRoot(),
preState.passportDscKeyRoot,
"Passport DSC key root changed"
);
assertEq(
passportRegistry.getPassportNoOfacRoot(),
preState.passportPassportNoOfacRoot,
"Passport passport# OFAC root changed"
);
assertEq(
passportRegistry.getNameAndDobOfacRoot(),
preState.passportNameDobOfacRoot,
"Passport name+DOB OFAC root changed"
);
assertEq(
passportRegistry.getNameAndYobOfacRoot(),
preState.passportNameYobOfacRoot,
"Passport name+YOB OFAC root changed"
);
assertEq(passportRegistry.getCscaRoot(), preState.passportCscaRoot, "Passport CSCA root changed");
console2.log("Passport Registry: 6/6 variables preserved");
// ID Card Registry state verification (5 variables)
assertEq(
idCardRegistry.getIdentityCommitmentMerkleRoot(),
preState.idCardIdentityRoot,
"ID Card identity root changed"
);
assertEq(
idCardRegistry.getDscKeyCommitmentMerkleRoot(),
preState.idCardDscKeyRoot,
"ID Card DSC key root changed"
);
assertEq(
idCardRegistry.getNameAndDobOfacRoot(),
preState.idCardNameDobOfacRoot,
"ID Card name+DOB OFAC root changed"
);
assertEq(
idCardRegistry.getNameAndYobOfacRoot(),
preState.idCardNameYobOfacRoot,
"ID Card name+YOB OFAC root changed"
);
assertEq(idCardRegistry.getCscaRoot(), preState.idCardCscaRoot, "ID Card CSCA root changed");
console2.log("ID Card Registry: 5/5 variables preserved");
// Aadhaar Registry state verification (3 variables)
assertEq(
aadhaarRegistry.getIdentityCommitmentMerkleRoot(),
preState.aadhaarIdentityRoot,
"Aadhaar identity root changed"
);
assertEq(
aadhaarRegistry.getNameAndDobOfacRoot(),
preState.aadhaarNameDobOfacRoot,
"Aadhaar name+DOB OFAC root changed"
);
assertEq(
aadhaarRegistry.getNameAndYobOfacRoot(),
preState.aadhaarNameYobOfacRoot,
"Aadhaar name+YOB OFAC root changed"
);
console2.log("Aadhaar Registry: 3/3 variables preserved");
console2.log("TOTAL: 18/18 state variables VERIFIED - NO storage collisions!");
console2.log("\n=== Phase 4: Verify Governance Roles ===");
// Deployer should have both roles initially
assertTrue(hub.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on Hub");
assertTrue(hub.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on Hub");
assertTrue(passportRegistry.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on Passport");
assertTrue(passportRegistry.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on Passport");
assertTrue(idCardRegistry.hasRole(SECURITY_ROLE, deployer), "Deployer missing SECURITY_ROLE on ID Card");
assertTrue(idCardRegistry.hasRole(OPERATIONS_ROLE, deployer), "Deployer missing OPERATIONS_ROLE on ID Card");
console2.log("Deployer has all required roles");
console2.log("\n=== Phase 5: Transfer Roles to Multisigs ===");
vm.startPrank(deployer);
// Grant roles to multisigs
hub.grantRole(SECURITY_ROLE, securityMultisig);
hub.grantRole(OPERATIONS_ROLE, operationsMultisig);
passportRegistry.grantRole(SECURITY_ROLE, securityMultisig);
passportRegistry.grantRole(OPERATIONS_ROLE, operationsMultisig);
idCardRegistry.grantRole(SECURITY_ROLE, securityMultisig);
idCardRegistry.grantRole(OPERATIONS_ROLE, operationsMultisig);
// Deployer renounces roles
hub.renounceRole(SECURITY_ROLE, deployer);
hub.renounceRole(OPERATIONS_ROLE, deployer);
passportRegistry.renounceRole(SECURITY_ROLE, deployer);
passportRegistry.renounceRole(OPERATIONS_ROLE, deployer);
idCardRegistry.renounceRole(SECURITY_ROLE, deployer);
idCardRegistry.renounceRole(OPERATIONS_ROLE, deployer);
vm.stopPrank();
console2.log("Roles transferred to multisigs");
console2.log("\n=== Phase 6: Verify Final State ===");
// Deployer should have NO roles
assertFalse(hub.hasRole(SECURITY_ROLE, deployer), "Deployer still has SECURITY_ROLE on Hub");
assertFalse(hub.hasRole(OPERATIONS_ROLE, deployer), "Deployer still has OPERATIONS_ROLE on Hub");
// Multisigs should have roles
assertTrue(hub.hasRole(SECURITY_ROLE, securityMultisig), "Critical multisig missing SECURITY_ROLE on Hub");
assertTrue(
hub.hasRole(OPERATIONS_ROLE, operationsMultisig),
"Standard multisig missing OPERATIONS_ROLE on Hub"
);
assertTrue(
passportRegistry.hasRole(SECURITY_ROLE, securityMultisig),
"Critical multisig missing SECURITY_ROLE on Passport"
);
assertTrue(
passportRegistry.hasRole(OPERATIONS_ROLE, operationsMultisig),
"Standard multisig missing OPERATIONS_ROLE on Passport"
);
assertTrue(
idCardRegistry.hasRole(SECURITY_ROLE, securityMultisig),
"Critical multisig missing SECURITY_ROLE on ID Card"
);
assertTrue(
idCardRegistry.hasRole(OPERATIONS_ROLE, operationsMultisig),
"Standard multisig missing OPERATIONS_ROLE on ID Card"
);
console2.log("Multisigs have full control");
console2.log("Deployer has ZERO control");
console2.log("\n================================================================================");
console2.log("UPGRADE TEST PASSED - Safe to execute on mainnet");
console2.log("================================================================================");
}
}