mirror of
https://github.com/selfxyz/self.git
synced 2026-01-08 22:28:11 -05:00
* 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
404 lines
15 KiB
TypeScript
404 lines
15 KiB
TypeScript
/**
|
|
* upgrade:prepare task
|
|
*
|
|
* Validates and deploys a new implementation contract.
|
|
* Does NOT execute the upgrade - that requires multisig approval.
|
|
*
|
|
* Features:
|
|
* - Auto-validates version increment (must be current + 1)
|
|
* - Auto-updates @custom:version in contract if needed
|
|
* - Auto-commits after successful deployment
|
|
* - Records git commit hash and creates tag
|
|
*
|
|
* Usage:
|
|
* npx hardhat upgrade:prepare --contract IdentityVerificationHub --network celo
|
|
*/
|
|
|
|
import { task, types } from "hardhat/config";
|
|
import { HardhatRuntimeEnvironment } from "hardhat/types";
|
|
import {
|
|
log,
|
|
getContractDefinition,
|
|
getProxyAddress,
|
|
getCurrentVersion,
|
|
getGitCommitShort,
|
|
getGitBranch,
|
|
hasUncommittedChanges,
|
|
validateVersionIncrement,
|
|
suggestNextVersion,
|
|
readContractVersion,
|
|
updateContractVersion,
|
|
getContractFilePath,
|
|
addVersion,
|
|
getExplorerUrl,
|
|
shortenAddress,
|
|
createGitTag,
|
|
gitCommit,
|
|
getLatestVersionInfo,
|
|
} from "./utils";
|
|
import { CONTRACT_IDS, ContractId, SupportedNetwork } from "./types";
|
|
|
|
interface PrepareTaskArgs {
|
|
contract: ContractId;
|
|
newVersion?: string;
|
|
changelog?: string;
|
|
dryRun: boolean;
|
|
skipCommit: boolean;
|
|
}
|
|
|
|
task("upgrade:prepare", "Validate and deploy a new implementation contract")
|
|
.addParam("contract", `Contract to upgrade (${CONTRACT_IDS.join(", ")})`, undefined, types.string)
|
|
.addOptionalParam(
|
|
"newVersion",
|
|
"New version - auto-detected from contract file if not provided",
|
|
undefined,
|
|
types.string,
|
|
)
|
|
.addOptionalParam("changelog", "Changelog entry for this version", undefined, types.string)
|
|
.addFlag("dryRun", "Simulate the deployment without actually deploying")
|
|
.addFlag("skipCommit", "Skip auto-commit after deployment")
|
|
.setAction(async (args: PrepareTaskArgs, hre: HardhatRuntimeEnvironment) => {
|
|
const { contract: contractId, changelog, dryRun, skipCommit } = args;
|
|
let { newVersion } = args;
|
|
const network = hre.network.name as SupportedNetwork;
|
|
|
|
log.header(`UPGRADE PREPARE: ${contractId}`);
|
|
log.detail("Network", network);
|
|
log.detail("Mode", dryRun ? "DRY RUN (no actual deployment)" : "LIVE DEPLOYMENT");
|
|
|
|
// ========================================================================
|
|
// Step 1: Validate inputs
|
|
// ========================================================================
|
|
log.step("Validating inputs...");
|
|
|
|
if (!CONTRACT_IDS.includes(contractId as ContractId)) {
|
|
log.error(`Invalid contract: ${contractId}`);
|
|
log.info(`Valid contracts: ${CONTRACT_IDS.join(", ")}`);
|
|
return;
|
|
}
|
|
|
|
const contractDef = getContractDefinition(contractId);
|
|
if (contractDef.type !== "uups-proxy") {
|
|
log.error(`Contract '${contractId}' is not upgradeable (type: ${contractDef.type})`);
|
|
return;
|
|
}
|
|
|
|
let proxyAddress: string;
|
|
try {
|
|
proxyAddress = getProxyAddress(contractId, network);
|
|
} catch {
|
|
log.error(`No proxy deployed for '${contractId}' on network '${network}'`);
|
|
log.info("Deploy the proxy first using the deploy script");
|
|
return;
|
|
}
|
|
|
|
const currentVersion = getCurrentVersion(contractId, network);
|
|
|
|
log.detail("Contract source", contractDef.source);
|
|
log.detail("Proxy address", proxyAddress);
|
|
log.detail("Current version", currentVersion);
|
|
|
|
// ========================================================================
|
|
// Step 2: Determine and validate new version
|
|
// ========================================================================
|
|
log.step("Validating version...");
|
|
|
|
const contractFilePath = getContractFilePath(contractId);
|
|
const contractFileVersion = contractFilePath ? readContractVersion(contractFilePath) : null;
|
|
|
|
if (contractFileVersion) {
|
|
log.detail("Version in contract file", contractFileVersion);
|
|
}
|
|
|
|
// If no version provided, use contract file version
|
|
if (!newVersion) {
|
|
if (contractFileVersion && contractFileVersion !== currentVersion) {
|
|
const validation = validateVersionIncrement(currentVersion, contractFileVersion);
|
|
if (validation.valid) {
|
|
newVersion = contractFileVersion;
|
|
log.info(`Using version from contract file: ${newVersion}`);
|
|
} else {
|
|
log.error(`Contract file has invalid version ${contractFileVersion}`);
|
|
const suggestions = suggestNextVersion(currentVersion);
|
|
log.info(`Current version: ${currentVersion}`);
|
|
log.info(
|
|
`Valid next versions: ${suggestions.patch} (patch), ${suggestions.minor} (minor), ${suggestions.major} (major)`,
|
|
);
|
|
return;
|
|
}
|
|
} else {
|
|
log.error("Contract file version matches current - update @custom:version in contract first");
|
|
const suggestions = suggestNextVersion(currentVersion);
|
|
log.info(`Current version: ${currentVersion}`);
|
|
log.info(
|
|
`Valid next versions: ${suggestions.patch} (patch), ${suggestions.minor} (minor), ${suggestions.major} (major)`,
|
|
);
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Validate version increment
|
|
const versionValidation = validateVersionIncrement(currentVersion, newVersion);
|
|
if (!versionValidation.valid) {
|
|
log.error(versionValidation.error!);
|
|
return;
|
|
}
|
|
|
|
log.success(`Version increment valid: ${currentVersion} → ${newVersion} (${versionValidation.type})`);
|
|
|
|
// ========================================================================
|
|
// Step 3: Update contract file version if needed
|
|
// ========================================================================
|
|
if (contractFilePath && contractFileVersion !== newVersion) {
|
|
log.step("Updating contract file version...");
|
|
|
|
if (dryRun) {
|
|
log.info(`[DRY RUN] Would update ${contractFilePath} to version ${newVersion}`);
|
|
} else {
|
|
const updated = updateContractVersion(contractFilePath, newVersion);
|
|
if (updated) {
|
|
log.success(`Updated @custom:version to ${newVersion} in contract file`);
|
|
} else {
|
|
log.warning("Could not update version in contract file - please update manually");
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// Step 4: Check git state
|
|
// ========================================================================
|
|
log.step("Checking git state...");
|
|
|
|
const gitBranch = getGitBranch();
|
|
const uncommittedChanges = hasUncommittedChanges();
|
|
|
|
log.detail("Branch", gitBranch);
|
|
|
|
if (uncommittedChanges && !dryRun) {
|
|
log.warning("You have uncommitted changes. They will be included in the auto-commit.");
|
|
}
|
|
|
|
// ========================================================================
|
|
// Step 5: Load and validate the new implementation
|
|
// ========================================================================
|
|
log.step("Loading contract factory...");
|
|
|
|
const contractName = contractDef.source;
|
|
let ContractFactory;
|
|
|
|
try {
|
|
// Handle contracts that need library linking
|
|
if (contractName === "IdentityVerificationHubImplV2") {
|
|
const CustomVerifier = await hre.ethers.getContractFactory("CustomVerifier");
|
|
const customVerifier = await CustomVerifier.deploy();
|
|
await customVerifier.waitForDeployment();
|
|
|
|
ContractFactory = await hre.ethers.getContractFactory(contractName, {
|
|
libraries: {
|
|
CustomVerifier: await customVerifier.getAddress(),
|
|
},
|
|
});
|
|
log.info("Deployed CustomVerifier library for linking");
|
|
} else if (
|
|
contractName === "IdentityRegistryImplV1" ||
|
|
contractName === "IdentityRegistryIdCardImplV1" ||
|
|
contractName === "IdentityRegistryAadhaarImplV1"
|
|
) {
|
|
const PoseidonT3 = await hre.ethers.getContractFactory("PoseidonT3");
|
|
const poseidonT3 = await PoseidonT3.deploy();
|
|
await poseidonT3.waitForDeployment();
|
|
|
|
ContractFactory = await hre.ethers.getContractFactory(contractName, {
|
|
libraries: {
|
|
PoseidonT3: await poseidonT3.getAddress(),
|
|
},
|
|
});
|
|
log.info("Deployed PoseidonT3 library for linking");
|
|
} else {
|
|
ContractFactory = await hre.ethers.getContractFactory(contractName);
|
|
}
|
|
|
|
log.success(`Loaded contract factory: ${contractName}`);
|
|
} catch (error) {
|
|
log.error(`Failed to load contract factory: ${error}`);
|
|
return;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Step 6: Validate storage layout
|
|
// ========================================================================
|
|
log.step("Validating storage layout compatibility...");
|
|
|
|
try {
|
|
await hre.upgrades.validateImplementation(ContractFactory, {
|
|
kind: "uups",
|
|
unsafeAllowLinkedLibraries: true,
|
|
unsafeAllow: ["constructor", "external-library-linking"],
|
|
});
|
|
log.success("Storage layout validation passed");
|
|
} catch (error) {
|
|
log.error(`Storage layout validation failed: ${error}`);
|
|
return;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Step 7: Simulate upgrade on fork
|
|
// ========================================================================
|
|
log.step("Simulating upgrade on fork...");
|
|
|
|
try {
|
|
const proxyContract = await hre.ethers.getContractAt(contractName, proxyAddress);
|
|
const SECURITY_ROLE = await proxyContract.SECURITY_ROLE();
|
|
log.detail("SECURITY_ROLE", SECURITY_ROLE);
|
|
log.success("Fork simulation passed - proxy is accessible");
|
|
} catch (error) {
|
|
log.error(`Fork simulation failed: ${error}`);
|
|
return;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Step 8: Dry run summary
|
|
// ========================================================================
|
|
if (dryRun) {
|
|
log.step("DRY RUN - Skipping actual deployment");
|
|
log.box([
|
|
"DRY RUN SUMMARY",
|
|
"─".repeat(50),
|
|
`Contract: ${contractId}`,
|
|
`Version: ${currentVersion} → ${newVersion}`,
|
|
`Network: ${network}`,
|
|
`Proxy: ${proxyAddress}`,
|
|
"",
|
|
"What would happen:",
|
|
`1. Update contract file to version ${newVersion}`,
|
|
"2. Deploy new implementation",
|
|
"3. Update registry.json",
|
|
"4. Create git commit and tag",
|
|
"",
|
|
"Run without --dry-run to execute.",
|
|
]);
|
|
return;
|
|
}
|
|
|
|
// ========================================================================
|
|
// Step 9: Deploy new implementation
|
|
// ========================================================================
|
|
log.step("Deploying new implementation...");
|
|
|
|
try {
|
|
const implementation = await ContractFactory.deploy();
|
|
await implementation.waitForDeployment();
|
|
const implementationAddress = await implementation.getAddress();
|
|
|
|
log.success(`Implementation deployed: ${implementationAddress}`);
|
|
log.detail("Explorer", `${getExplorerUrl(network)}/address/${implementationAddress}`);
|
|
|
|
// ========================================================================
|
|
// Step 10: Verify on block explorer
|
|
// ========================================================================
|
|
log.step("Verifying contract on block explorer...");
|
|
|
|
try {
|
|
await hre.run("verify:verify", {
|
|
address: implementationAddress,
|
|
constructorArguments: [],
|
|
});
|
|
log.success("Contract verified on block explorer");
|
|
} catch (error: any) {
|
|
if (error.message?.includes("Already Verified")) {
|
|
log.info("Contract already verified");
|
|
} else {
|
|
log.warning(`Verification failed: ${error.message}`);
|
|
log.info("You can verify manually later");
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// Step 11: Update registry
|
|
// ========================================================================
|
|
log.step("Updating deployment registry...");
|
|
|
|
// Get previous version info to determine initializer version
|
|
const latestVersion = getLatestVersionInfo(contractId);
|
|
const newInitializerVersion = (latestVersion?.info.initializerVersion || 0) + 1;
|
|
|
|
const gitCommitShort = getGitCommitShort();
|
|
const deployerAddress = (await hre.ethers.provider.getSigner()).address;
|
|
|
|
addVersion(
|
|
contractId,
|
|
network,
|
|
newVersion,
|
|
{
|
|
initializerVersion: newInitializerVersion,
|
|
initializerFunction: newInitializerVersion === 1 ? "initialize" : `initializeV${newInitializerVersion}`,
|
|
changelog: changelog || `Upgrade to v${newVersion}`,
|
|
gitTag: `${contractId.toLowerCase()}-v${newVersion}`,
|
|
},
|
|
{
|
|
impl: implementationAddress,
|
|
deployedAt: new Date().toISOString(),
|
|
deployedBy: deployerAddress,
|
|
gitCommit: "", // Will be set after commit
|
|
},
|
|
);
|
|
log.success("Registry updated");
|
|
|
|
// ========================================================================
|
|
// Step 12: Auto-commit and tag
|
|
// ========================================================================
|
|
if (!skipCommit) {
|
|
log.step("Creating git commit...");
|
|
|
|
const commitMessage = `feat: ${contractId} v${newVersion} deployed on ${network.charAt(0).toUpperCase() + network.slice(1)}
|
|
|
|
- Implementation: ${implementationAddress}
|
|
- Changelog: ${changelog || "Upgrade"}`;
|
|
|
|
const committed = gitCommit(commitMessage);
|
|
if (committed) {
|
|
const newGitCommit = getGitCommitShort();
|
|
log.success(`Committed: ${newGitCommit}`);
|
|
|
|
// Try to create git tag
|
|
try {
|
|
createGitTag(
|
|
`${contractId.toLowerCase()}-v${newVersion}`,
|
|
`${contractId} v${newVersion} - ${changelog || "Upgrade"}`,
|
|
);
|
|
log.success(`Created git tag: ${contractId.toLowerCase()}-v${newVersion}`);
|
|
} catch (e) {
|
|
log.warning("Could not create git tag - you can create it manually");
|
|
}
|
|
} else {
|
|
log.warning("Could not create git commit - please commit manually");
|
|
}
|
|
}
|
|
|
|
// ========================================================================
|
|
// Summary
|
|
// ========================================================================
|
|
log.box([
|
|
"DEPLOYMENT SUCCESSFUL",
|
|
"═".repeat(50),
|
|
`Contract: ${contractId}`,
|
|
`Version: ${currentVersion} → ${newVersion}`,
|
|
`Network: ${network}`,
|
|
"",
|
|
"Addresses:",
|
|
` Proxy: ${shortenAddress(proxyAddress)}`,
|
|
` New Impl: ${shortenAddress(implementationAddress)}`,
|
|
"",
|
|
"Next steps:",
|
|
` 1. Run: npx hardhat upgrade:propose --contract ${contractId} --network ${network}`,
|
|
" 2. Multisig signers approve in Safe UI",
|
|
" 3. Transaction executes automatically when threshold reached",
|
|
]);
|
|
} catch (error) {
|
|
log.error(`Deployment failed: ${error}`);
|
|
return;
|
|
}
|
|
});
|
|
|
|
export {};
|