From 243690ea11fb91eb8bad3aea5539c1a19720e740 Mon Sep 17 00:00:00 2001 From: 0xmad <0xmad@users.noreply.github.com> Date: Tue, 30 Jan 2024 12:23:11 -0600 Subject: [PATCH] docs(contracts): add documentation for deployment workflow - [x] Add documentation for deploy steps - [x] Add documentation for helpers - [x] Add documentation for runners - [x] Add some notes for task imports --- contracts/hardhat.config.ts | 1 + contracts/tasks/deploy/index.ts | 3 + .../01-constantInitialVoiceCreditProxy.ts | 3 + .../maci/02-freeForAllSignUpGatekeeper.ts | 3 + contracts/tasks/deploy/maci/03-verifier.ts | 3 + contracts/tasks/deploy/maci/04-topupCredit.ts | 3 + contracts/tasks/deploy/maci/05-poseidon.ts | 3 + contracts/tasks/deploy/maci/06-pollFactory.ts | 3 + .../deploy/maci/07-messageProcessorFactory.ts | 3 + .../tasks/deploy/maci/08-tallyFactory.ts | 3 + .../tasks/deploy/maci/09-subsidyFactory.ts | 3 + contracts/tasks/deploy/maci/10-maci.ts | 3 + contracts/tasks/helpers/ContractStorage.ts | 79 ++++++++++++++++- contracts/tasks/helpers/ContractVerifier.ts | 19 ++++ contracts/tasks/helpers/Deployment.ts | 88 +++++++++++++++++++ contracts/tasks/helpers/constants.ts | 20 +++++ contracts/tasks/runner/deployFull.ts | 4 + contracts/tasks/runner/verifyFull.ts | 3 + 18 files changed, 245 insertions(+), 2 deletions(-) diff --git a/contracts/hardhat.config.ts b/contracts/hardhat.config.ts index 358e954399..aa6adca098 100644 --- a/contracts/hardhat.config.ts +++ b/contracts/hardhat.config.ts @@ -7,6 +7,7 @@ import "solidity-docgen"; import type { HardhatUserConfig } from "hardhat/config"; +// Don't forget to import new tasks here import "./tasks/deploy"; import { EChainId, ESupportedChains, NETWORKS_DEFAULT_GAS, getNetworkRpcUrls } from "./tasks/helpers/constants"; import "./tasks/runner/deployFull"; diff --git a/contracts/tasks/deploy/index.ts b/contracts/tasks/deploy/index.ts index 54c25cf902..251930a18d 100644 --- a/contracts/tasks/deploy/index.ts +++ b/contracts/tasks/deploy/index.ts @@ -1,6 +1,9 @@ import fs from "fs"; import path from "path"; +/** + * The same as individual imports but doesn't require to add new import line everytime + */ ["maci"].forEach((folder) => { const tasksPath = path.resolve(__dirname, folder); diff --git a/contracts/tasks/deploy/maci/01-constantInitialVoiceCreditProxy.ts b/contracts/tasks/deploy/maci/01-constantInitialVoiceCreditProxy.ts index 42e9ee70b8..455f92934c 100644 --- a/contracts/tasks/deploy/maci/01-constantInitialVoiceCreditProxy.ts +++ b/contracts/tasks/deploy/maci/01-constantInitialVoiceCreditProxy.ts @@ -7,6 +7,9 @@ const DEFAULT_INITIAL_VOICE_CREDITS = 99; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-constant-initial-voice-credit-proxy", "Deploy constant initial voice credit proxy") .setAction( diff --git a/contracts/tasks/deploy/maci/02-freeForAllSignUpGatekeeper.ts b/contracts/tasks/deploy/maci/02-freeForAllSignUpGatekeeper.ts index c1d9e6807f..07960972c5 100644 --- a/contracts/tasks/deploy/maci/02-freeForAllSignUpGatekeeper.ts +++ b/contracts/tasks/deploy/maci/02-freeForAllSignUpGatekeeper.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-free-for-all-signup-gatekeeper", "Deploy constant initial voice credit proxy") .setAction(async ({ incremental }: IDeployParams, hre) => { diff --git a/contracts/tasks/deploy/maci/03-verifier.ts b/contracts/tasks/deploy/maci/03-verifier.ts index c526a639b8..59eb50e8ef 100644 --- a/contracts/tasks/deploy/maci/03-verifier.ts +++ b/contracts/tasks/deploy/maci/03-verifier.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-verifier", "Deploy verifier") .setAction(async ({ incremental }: IDeployParams, hre) => { diff --git a/contracts/tasks/deploy/maci/04-topupCredit.ts b/contracts/tasks/deploy/maci/04-topupCredit.ts index 78d0703eec..31709672c3 100644 --- a/contracts/tasks/deploy/maci/04-topupCredit.ts +++ b/contracts/tasks/deploy/maci/04-topupCredit.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-topup-credit", "Deploy topup credit") .setAction(async ({ incremental }: IDeployParams, hre) => { diff --git a/contracts/tasks/deploy/maci/05-poseidon.ts b/contracts/tasks/deploy/maci/05-poseidon.ts index 34b4a6d123..16e374c27c 100644 --- a/contracts/tasks/deploy/maci/05-poseidon.ts +++ b/contracts/tasks/deploy/maci/05-poseidon.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-poseidon", "Deploy poseidon contracts") .setAction(async ({ incremental }: IDeployParams, hre) => { diff --git a/contracts/tasks/deploy/maci/06-pollFactory.ts b/contracts/tasks/deploy/maci/06-pollFactory.ts index ba4ceae4cc..e48828db68 100644 --- a/contracts/tasks/deploy/maci/06-pollFactory.ts +++ b/contracts/tasks/deploy/maci/06-pollFactory.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-poll-factory", "Deploy poll factory") .setAction(async ({ incremental }: IDeployParams, hre) => { diff --git a/contracts/tasks/deploy/maci/07-messageProcessorFactory.ts b/contracts/tasks/deploy/maci/07-messageProcessorFactory.ts index 9baf24dbd9..640e22c079 100644 --- a/contracts/tasks/deploy/maci/07-messageProcessorFactory.ts +++ b/contracts/tasks/deploy/maci/07-messageProcessorFactory.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-message-processor-factory", "Deploy message processor factory") .setAction(async ({ incremental }: IDeployParams, hre) => { diff --git a/contracts/tasks/deploy/maci/08-tallyFactory.ts b/contracts/tasks/deploy/maci/08-tallyFactory.ts index 14f06200ed..090b78c590 100644 --- a/contracts/tasks/deploy/maci/08-tallyFactory.ts +++ b/contracts/tasks/deploy/maci/08-tallyFactory.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-tally-factory", "Deploy tally factory") .setAction(async ({ incremental }: IDeployParams, hre) => { diff --git a/contracts/tasks/deploy/maci/09-subsidyFactory.ts b/contracts/tasks/deploy/maci/09-subsidyFactory.ts index 4e4cd09df1..455bdacbf0 100644 --- a/contracts/tasks/deploy/maci/09-subsidyFactory.ts +++ b/contracts/tasks/deploy/maci/09-subsidyFactory.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-subsidy-factory", "Deploy subsidy factory") .setAction(async ({ incremental }: IDeployParams, hre) => { diff --git a/contracts/tasks/deploy/maci/10-maci.ts b/contracts/tasks/deploy/maci/10-maci.ts index b93f70e538..891c874bc7 100644 --- a/contracts/tasks/deploy/maci/10-maci.ts +++ b/contracts/tasks/deploy/maci/10-maci.ts @@ -5,6 +5,9 @@ import { EContracts, IDeployParams } from "../../helpers/types"; const deployment = Deployment.getInstance(); const storage = ContractStorage.getInstance(); +/** + * Deploy step registration and task itself + */ deployment .deployTask("full:deploy-maci", "Deploy MACI contract") .setAction(async ({ incremental, stateTreeDepth }: IDeployParams, hre) => { diff --git a/contracts/tasks/helpers/ContractStorage.ts b/contracts/tasks/helpers/ContractStorage.ts index 6ac8d9a98a..b2365bac59 100644 --- a/contracts/tasks/helpers/ContractStorage.ts +++ b/contracts/tasks/helpers/ContractStorage.ts @@ -1,10 +1,15 @@ -/* eslint-disable import/no-extraneous-dependencies */ -/* eslint-disable no-console */ +/* eslint-disable import/no-extraneous-dependencies, no-console */ import low from "lowdb"; import FileSync from "lowdb/adapters/FileSync"; import type { EContracts, IRegisterContract, IStorageInstanceEntry, IStorageNamedEntry } from "./types"; +/** + * Internal storage structure type. + * named: contracts can be queried by name + * instance: contract can be queried by address + * verified: mark contracts which are already verified + */ type TStorage = Record< string, Partial<{ @@ -14,15 +19,33 @@ type TStorage = Record< }> >; +/** + * @notice Contract storage keeps all deployed contracts with addresses, arguments in the json file. + * This class is using for incremental deployment and verification. + */ export class ContractStorage { + /** + * Singleton instance for class + */ private static INSTANCE?: ContractStorage; + /** + * Json file database instance + */ private db: low.LowdbSync; + /** + * Initialize class properties only once + */ private constructor() { this.db = low(new FileSync("./deployed-contracts.json")); } + /** + * Get singleton object + * + * @returns {ContractStorage} singleton object + */ static getInstance(): ContractStorage { if (!ContractStorage.INSTANCE) { ContractStorage.INSTANCE = new ContractStorage(); @@ -31,6 +54,11 @@ export class ContractStorage { return ContractStorage.INSTANCE; } + /** + * Register contract and save contract address, constructor args in the json file + * + * @param {IRegisterContract} args - register arguments + */ async register({ id, contract, network, args }: IRegisterContract): Promise { const contractAddress = await contract.getAddress(); @@ -72,6 +100,12 @@ export class ContractStorage { .write(); } + /** + * Get contract instances from the json file + * + * @param network - selected network + * @returns {[string, IStorageInstanceEntry][]} storage instance entries + */ getInstances(network: string): [string, IStorageInstanceEntry][] { const collection = this.db.get(`${network}.instance`); const value = collection.value() as IStorageInstanceEntry[] | undefined; @@ -79,14 +113,35 @@ export class ContractStorage { return Object.entries(value || []); } + /** + * Check if contract is verified or not locally + * + * @param address - contract address + * @param network - selected network + * @returns contract verified or not + */ getVerified(address: string, network: string): boolean { return this.db.get(`${network}.verified.${address}`).value() as unknown as boolean; } + /** + * Set contract verification in the json file + * + * @param address - contract address + * @param network - selected network + * @param verified - verified or not + */ setVerified = (address: string, network: string, verified: boolean): void => { this.db.set(`${network}.verified.${address}`, verified).write(); }; + /** + * Get contract address by name from the json file + * + * @param id - contract name + * @param network - selected network + * @returns contract address + */ getAddress(id: EContracts, network: string): string | undefined { const collection = this.db.get(`${network}.named.${id}`); const namedEntry = collection.value() as IStorageNamedEntry | undefined; @@ -94,6 +149,14 @@ export class ContractStorage { return namedEntry?.address; } + /** + * Get contract address by name from the json file + * + * @param id - contract name + * @param network - selected network + * @throws {Error} if there is no address the error will be thrown + * @returns contract address + */ mustGetAddress(id: EContracts, network: string): string { const address = this.getAddress(id, network); @@ -104,6 +167,13 @@ export class ContractStorage { return address; } + /** + * Get contract from the json file with sizes and multi count + * + * @param deployer - deployer address + * @param network - selected network + * @returns {[entries: Map, length: number, multiCount: number]} + */ printContracts(deployer: string, network: string): [Map, number, number] { console.log("Contracts deployed at", network, "by", deployer); console.log("---------------------------------"); @@ -135,6 +205,11 @@ export class ContractStorage { return [entryMap, instanceEntries.length, multiCount]; } + /** + * Clean json file for selected network + * + * @param network - selected network + */ cleanup(network: string): void { this.db.set(network, {}).write(); } diff --git a/contracts/tasks/helpers/ContractVerifier.ts b/contracts/tasks/helpers/ContractVerifier.ts index 2e7915ceb6..b98d410184 100644 --- a/contracts/tasks/helpers/ContractVerifier.ts +++ b/contracts/tasks/helpers/ContractVerifier.ts @@ -1,13 +1,32 @@ import type { IVerificationSubtaskArgs } from "./types"; import type { HardhatRuntimeEnvironment, Libraries } from "hardhat/types"; +/** + * @notice Contract verifier allows to verify contract using hardhat-etherscan plugin. + */ export class ContractVerifier { + /** + * Hardhat runtime environment + */ private hre: HardhatRuntimeEnvironment; + /** + * Initialize class properties + * + * @param hre - Hardhat runtime environment + */ constructor(hre: HardhatRuntimeEnvironment) { this.hre = hre; } + /** + * Verify contract through etherscan + * + * @param address - contract address + * @param constructorArguments - stringified constructor arguments + * @param libraries - stringified libraries which can't be detected automatically + * @returns + */ async verify(address: string, constructorArguments: string, libraries?: string): Promise<[boolean, string]> { const params: IVerificationSubtaskArgs = { address, diff --git a/contracts/tasks/helpers/Deployment.ts b/contracts/tasks/helpers/Deployment.ts index 9db047791b..94f6785a23 100644 --- a/contracts/tasks/helpers/Deployment.ts +++ b/contracts/tasks/helpers/Deployment.ts @@ -5,18 +5,38 @@ import type { EContracts, IDeployParams, IDeployStep, IDeployStepCatalog } from import type { HardhatEthersSigner } from "@nomicfoundation/hardhat-ethers/signers"; import type { ConfigurableTaskDefinition, HardhatRuntimeEnvironment, TaskArguments } from "hardhat/types"; +/** + * @notice Deployment helper class to run sequential deploy using steps and deploy contracts. + */ export class Deployment { + /** + * Singleton instance for class + */ private static INSTANCE?: Deployment; + /** + * Hardhat runtime environment + */ private hre?: HardhatRuntimeEnvironment; + /** + * Step catalog to create sequential tasks + */ private stepCatalog: Map; + /** + * Initialize class properties only once + */ private constructor(hre?: HardhatRuntimeEnvironment) { this.stepCatalog = new Map([["full", []]]); this.hre = hre; } + /** + * Get singleton object + * + * @returns {ContractStorage} singleton object + */ static getInstance(hre?: HardhatRuntimeEnvironment): Deployment { if (!Deployment.INSTANCE) { Deployment.INSTANCE = new Deployment(hre); @@ -25,6 +45,11 @@ export class Deployment { return Deployment.INSTANCE; } + /** + * Get deployer (first signer) from hardhat runtime environment + * + * @returns {Promise} - signer + */ async getDeployer(): Promise { this.checkHre(); @@ -33,16 +58,34 @@ export class Deployment { return deployer; } + /** + * Set hardhat runtime environment + * + * @param hre - hardhat runtime environment + */ setHre(hre: HardhatRuntimeEnvironment): void { this.hre = hre; } + /** + * Check if hardhat runtime environment is set + * + * @throws {Error} error if there is no hardhat runtime environment set + */ private checkHre(): void { if (!this.hre) { throw new Error("Hardhat Runtime Environment is not set"); } } + /** + * Register deploy task by updating step catalog and return task definition + * + * @param taskName - unique task name + * @param stepName - task description + * @param paramsFn - optional function to override default task arguments + * @returns {ConfigurableTaskDefinition} hardhat task definition + */ deployTask( taskName: string, stepName: string, @@ -54,6 +97,12 @@ export class Deployment { return task(taskName, stepName); } + /** + * Register deployment step + * + * @param deployType - deploy type + * @param {IDeployStepCatalog} - deploy step catalog name, description and param mapper + */ private addStep(deployType: string, { name, taskName, paramsFn }: IDeployStepCatalog): void { const steps = this.stepCatalog.get(deployType); @@ -64,9 +113,22 @@ export class Deployment { steps.push({ name, taskName, paramsFn }); } + /** + * Get default params from hardhat task + * + * @param {IDeployParams} params - hardhat task arguments + * @returns {Promise} params for deploy workflow + */ private getDefaultParams = ({ verify, incremental, amount, stateTreeDepth }: IDeployParams): Promise => Promise.resolve({ verify, incremental, amount, stateTreeDepth }); + /** + * Get deploy step sequence + * + * @param deployType - deploy type + * @param {IDeployParams} params - deploy params + * @returns {Promise} deploy steps + */ getDeploySteps = async (deployType: string, params: IDeployParams): Promise => { const stepList = this.stepCatalog.get(deployType); @@ -84,6 +146,14 @@ export class Deployment { ); }; + /** + * Deploy contract and return it + * + * @param contractName - contract name + * @param signer - signer + * @param args - constructor arguments + * @returns deployed contract + */ async deployContract( contractName: EContracts, signer?: Signer, @@ -104,6 +174,13 @@ export class Deployment { return contract as unknown as T; } + /** + * Deploy contract with linked libraries using contract factory + * + * @param contractFactory - ethers contract factory + * @param args - constructor arguments + * @returns deployed contract + */ async deployContractWithLinkedLibraries( contractFactory: ContractFactory, ...args: unknown[] @@ -120,6 +197,17 @@ export class Deployment { return contract as T; } + /** + * Link poseidon libraries with contract factory and return it + * + * @param name - contract name + * @param poseidonT3Address - PoseidonT3 contract address + * @param poseidonT4Address - PoseidonT4 contract address + * @param poseidonT5Address - PoseidonT5 contract address + * @param poseidonT6Address - PoseidonT6 contract address + * @param signer - signer + * @returns contract factory with linked libraries + */ async linkPoseidonLibraries( name: EContracts, poseidonT3Address: string, diff --git a/contracts/tasks/helpers/constants.ts b/contracts/tasks/helpers/constants.ts index 90a2553aab..127aa37596 100644 --- a/contracts/tasks/helpers/constants.ts +++ b/contracts/tasks/helpers/constants.ts @@ -1,3 +1,6 @@ +/** + * Supported networks for deployment and task running + */ export enum ESupportedChains { Sepolia = "sepolia", Goerli = "goerli", @@ -9,6 +12,9 @@ export enum ESupportedChains { Optimism = "optimism", } +/** + * Supported network chain ids for deployment and task running + */ export enum EChainId { Hardhat = 31337, Goerli = 5, @@ -22,8 +28,17 @@ export enum EChainId { const GWEI = 1e9; +/** + * Convert gas price from gwei to wei + * + * @param value - gas price in gwei + * @returns gas price in wei + */ const gasPrice = (value: number) => value * GWEI; +/** + * Gas price settings for supported network + */ export const NETWORKS_DEFAULT_GAS: Record = { [ESupportedChains.Sepolia]: gasPrice(2), [ESupportedChains.Goerli]: gasPrice(2), @@ -35,6 +50,11 @@ export const NETWORKS_DEFAULT_GAS: Record = { [ESupportedChains.Optimism]: "auto", }; +/** + * Get network rpc urls object + * + * @returns {Record} rpc urls for supported networks + */ export const getNetworkRpcUrls = (): Record => { const INFURA_KEY = process.env.INFURA_KEY ?? ""; diff --git a/contracts/tasks/runner/deployFull.ts b/contracts/tasks/runner/deployFull.ts index 217a73e3fc..bf1c26fec5 100644 --- a/contracts/tasks/runner/deployFull.ts +++ b/contracts/tasks/runner/deployFull.ts @@ -8,6 +8,10 @@ import type { IDeployParams } from "../helpers/types"; import { ContractStorage } from "../helpers/ContractStorage"; import { Deployment } from "../helpers/Deployment"; +/** + * Main deployment task which runs deploy steps in the same order that `Deployment#deployTask` is called. + * Note: you probably need to use indicies for deployment step files to support the correct order. + */ task("deploy-full", "Deploy environment") .addFlag("incremental", "Incremental deployment") .addFlag("strict", "Fail on warnings") diff --git a/contracts/tasks/runner/verifyFull.ts b/contracts/tasks/runner/verifyFull.ts index e0bd9f7d1b..1aa97bd7b9 100644 --- a/contracts/tasks/runner/verifyFull.ts +++ b/contracts/tasks/runner/verifyFull.ts @@ -6,6 +6,9 @@ import type { IStorageInstanceEntry, IVerifyFullArgs } from "../helpers/types"; import { ContractStorage } from "../helpers/ContractStorage"; import { ContractVerifier } from "../helpers/ContractVerifier"; +/** + * Main verification task which runs hardhat-etherscan task for all the deployed contract. + */ task("verify-full", "Verify contracts listed in storage") .addFlag("force", "Ignore verified status") .setAction(async ({ force = false }: IVerifyFullArgs, hre) => {