diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 7cf6f23d5..343232f16 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -32,11 +32,9 @@ jobs: run: npm run build - name: Check storage layout run: npm run test-storage-layout - - name: Run deployment - # Basic deployment to make sure everything is ok. - # Could be removed in the future if not relevant. + - name: Test deployment run: npm run deploy - - name: Test Timelock Deployment + - name: Test Timelock deployment run: npm run deploy:timelock - name: Run coverage run: npm run coverage diff --git a/CHANGELOG.md b/CHANGELOG.md index e80f58d40..53804462f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - [x] `IexecPoco2Delegate.sol` ### Features +- Refactor Factory deployer (#206) - Enable native tests on CI (#204) - Migrate to Ethers v6: - Deployment scripts (#187, #203) diff --git a/deploy/0_deploy.ts b/deploy/0_deploy.ts index 7675fe189..93a871267 100644 --- a/deploy/0_deploy.ts +++ b/deploy/0_deploy.ts @@ -38,7 +38,7 @@ import { } from '../typechain'; import { Ownable__factory } from '../typechain/factories/@openzeppelin/contracts/access'; import config from '../utils/config'; -import { FactoryDeployerHelper } from '../utils/FactoryDeployerHelper'; +import { FactoryDeployer } from '../utils/FactoryDeployer'; import { linkContractToProxy } from '../utils/proxy-tools'; /** @@ -57,7 +57,7 @@ export default async function deploy() { const [owner] = await ethers.getSigners(); const deploymentOptions = config.getChainConfigOrDefault(chainId); const salt = process.env.SALT || deploymentOptions.v5.salt || ethers.ZeroHash; - const factoryDeployer = new FactoryDeployerHelper(owner, salt); + const factoryDeployer = new FactoryDeployer(owner, salt); // Deploy RLC const isTokenMode = !config.isNativeChain(deploymentOptions); let rlcInstanceAddress = isTokenMode diff --git a/scripts/deploy-timelock.ts b/scripts/deploy-timelock.ts index 2f0f0f849..9fad81e83 100644 --- a/scripts/deploy-timelock.ts +++ b/scripts/deploy-timelock.ts @@ -4,7 +4,7 @@ import { duration } from '@nomicfoundation/hardhat-network-helpers/dist/src/helpers/time'; import { ethers } from 'hardhat'; import { TimelockController__factory } from '../typechain'; -import { FactoryDeployerHelper } from '../utils/FactoryDeployerHelper'; +import { FactoryDeployer } from '../utils/FactoryDeployer'; import config from '../utils/config'; /** @@ -18,7 +18,7 @@ export const deploy = async () => { const salt = process.env.SALT || config.getChainConfigOrDefault(chainId).v5.salt; // Initialize factory deployer - const factoryDeployer = new FactoryDeployerHelper(owner, salt); + const factoryDeployer = new FactoryDeployer(owner, salt); // Deploy TimelockController const ONE_WEEK_IN_SECONDS = duration.days(7); diff --git a/utils/FactoryDeployer.ts b/utils/FactoryDeployer.ts index 53f878d79..4eda5e3c4 100644 --- a/utils/FactoryDeployer.ts +++ b/utils/FactoryDeployer.ts @@ -4,57 +4,98 @@ import factoryJson from '@amxx/factory/deployments/GenericFactory.json'; import factoryShanghaiJson from '@amxx/factory/deployments/GenericFactory_shanghai.json'; import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; -import { Contract, ethers } from 'ethers'; -import hre from 'hardhat'; +import { ContractFactory } from 'ethers'; +import hre, { deployments, ethers } from 'hardhat'; +import { GenericFactory, GenericFactory__factory } from '../typechain'; import config from './config'; +import { getBaseNameFromContractFactory } from './deploy-tools'; -interface FactoryConfig { - address: string; - deployer: string; - cost: string; - tx: string; - abi: any[]; -} - -const factoryConfig: FactoryConfig = - !config.isNativeChain() && hre.network.name.includes('hardhat') - ? factoryShanghaiJson - : factoryJson; +export class FactoryDeployer { + owner: SignerWithAddress; + salt: string; + genericFactory!: GenericFactory; -export class EthersDeployer { - private factory!: Contract; - private factoryAsPromise: Promise; + constructor(owner: SignerWithAddress, salt: string) { + this.owner = owner; + this.salt = salt; + } - constructor(wallet: SignerWithAddress) { - this.factoryAsPromise = new Promise(async (resolve, reject) => { - if ((await wallet.provider!.getCode(factoryConfig.address)) !== '0x') { - console.log(`→ Factory is available on this network`); - } else { - try { - console.log(`→ Factory is not yet deployed on this network`); - await wallet - .sendTransaction({ - to: factoryConfig.deployer, - value: factoryConfig.cost, - }) - .then((tx) => tx.wait()); - await wallet.provider - .broadcastTransaction(factoryConfig.tx) - .then((tx) => tx.wait()); - console.log(`→ Factory successfully deployed`); - } catch (e) { - console.log(`→ Error deploying the factory`); - reject(e); - } - } - this.factory = new ethers.Contract(factoryConfig.address, factoryConfig.abi, wallet); - resolve(this.factory); + /** + * Deploy a contract through GenericFactory [and optionally trigger a call] + */ + async deployWithFactory( + contractFactory: ContractFactory, + constructorArgs?: any[], + call?: string, + ) { + await this.init(); + let bytecode = (await contractFactory.getDeployTransaction(...(constructorArgs ?? []))) + .data; + if (!bytecode) { + throw new Error('Failed to prepare bytecode'); + } + let contractAddress = await (call + ? this.genericFactory.predictAddressWithCall(bytecode, this.salt, call) + : this.genericFactory.predictAddress(bytecode, this.salt)); + const previouslyDeployed = (await ethers.provider.getCode(contractAddress)) !== '0x'; + if (!previouslyDeployed) { + await ( + call + ? this.genericFactory.createContractAndCall(bytecode, this.salt, call) + : this.genericFactory.createContract(bytecode, this.salt) + ).then((tx) => tx.wait()); + } + const contractName = getBaseNameFromContractFactory(contractFactory); + console.log( + `${contractName}: ${contractAddress} ${ + previouslyDeployed ? ' (previously deployed)' : '' + }`, + ); + await deployments.save(contractName, { + // abi field is not used but is a required arg. Empty abi would be fine + abi: (contractFactory as any).constructor.abi, + address: contractAddress, + bytecode: bytecode, + args: constructorArgs, }); + return contractAddress; } - async ready(): Promise { - await this.factoryAsPromise; + private async init() { + if (this.genericFactory) { + // Already initialized. + return; + } + const factoryConfig: FactoryConfig = + !config.isNativeChain() && hre.network.name.includes('hardhat') + ? factoryShanghaiJson + : factoryJson; + this.genericFactory = GenericFactory__factory.connect(factoryConfig.address, this.owner); + if ((await ethers.provider.getCode(factoryConfig.address)) !== '0x') { + console.log(`→ Factory is available on this network`); + return; + } + try { + console.log(`→ Factory is not yet deployed on this network`); + await this.owner + .sendTransaction({ + to: factoryConfig.deployer, + value: factoryConfig.cost, + }) + .then((tx) => tx.wait()); + await ethers.provider.broadcastTransaction(factoryConfig.tx).then((tx) => tx.wait()); + console.log(`→ Factory successfully deployed`); + } catch (e) { + console.log(e); + throw new Error('→ Error deploying the factory'); + } } } -export const factoryAddress = factoryConfig.address; +interface FactoryConfig { + address: string; + deployer: string; + cost: string; + tx: string; + abi: any[]; +} diff --git a/utils/FactoryDeployerHelper.ts b/utils/FactoryDeployerHelper.ts deleted file mode 100644 index 80809fd97..000000000 --- a/utils/FactoryDeployerHelper.ts +++ /dev/null @@ -1,69 +0,0 @@ -// SPDX-FileCopyrightText: 2024-2025 IEXEC BLOCKCHAIN TECH -// SPDX-License-Identifier: Apache-2.0 - -import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; -import { ContractFactory } from 'ethers'; -import { deployments, ethers } from 'hardhat'; -import { GenericFactory, GenericFactory__factory } from '../typechain'; -import { getBaseNameFromContractFactory } from './deploy-tools'; -const { EthersDeployer: Deployer, factoryAddress } = require('../utils/FactoryDeployer'); - -// TODO merge FactoryDeployer and FactoryDeployerHelper here. -// Use: -// async getFactory() { -// if (!this.genericFactory) { -// await this.ready(); -// } -// return this.genericFactory; -// } - -export class FactoryDeployerHelper { - salt: string; - init: any; - genericFactory: GenericFactory; - - constructor(owner: SignerWithAddress, salt: string) { - this.salt = salt; - this.init = new Deployer(owner); - this.genericFactory = GenericFactory__factory.connect(factoryAddress, owner); - } - - /** - * Deploy a contract through GenericFactory [and optionally trigger a call] - */ - async deployWithFactory( - contractFactory: ContractFactory, - constructorArgs?: any[], - call?: string, - ) { - await this.init.ready(); // Deploy GenericFactory if not already done - let bytecode = (await contractFactory.getDeployTransaction(...(constructorArgs ?? []))) - .data; - if (!bytecode) { - throw new Error('Failed to prepare bytecode'); - } - let contractAddress = await (call - ? this.genericFactory.predictAddressWithCall(bytecode, this.salt, call) - : this.genericFactory.predictAddress(bytecode, this.salt)); - const previouslyDeployed = (await ethers.provider.getCode(contractAddress)) !== '0x'; - if (!previouslyDeployed) { - await ( - call - ? this.genericFactory.createContractAndCall(bytecode, this.salt, call) - : this.genericFactory.createContract(bytecode, this.salt) - ).then((tx) => tx.wait()); - } - const contractName = getBaseNameFromContractFactory(contractFactory); - console.log( - `${contractName}: ${contractAddress} ${ - previouslyDeployed ? ' (previously deployed)' : '' - }`, - ); - await deployments.save(contractName, { - // abi field is not used but is a required arg. Empty abi would be fine - abi: (contractFactory as any).constructor.abi, - address: contractAddress, - }); - return contractAddress; - } -}