diff --git a/CHANGELOG.md b/CHANGELOG.md index 11d645203..588b03262 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,8 @@ - [x] `IexecPoco2Delegate.sol` ### Features - +- Add CreateX factory for new chain deployment (#215) +- Support Arbitrum & Avalanche Fuji testnets (#215) - Housekeeping (#207, ) - Add Halborn "Poco v5.5 & Voucher v1.0" audit report (#205) - Refactor Factory deployer (#206) diff --git a/README.md b/README.md index 93fea986f..3221d8a7d 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ It contains: - **"asset":** can be "Token" or "Native", select which escrow to use. - **"token":** the address of the token to use. If asset is set to token, and no token address is provided, a mock will be deployed on the fly. - **"v3":** a list of resources from a previous (v3) deployment. This allows previous resources to be automatically available. It also enables score transfer from v3 to v5. [optional] - - **"v5":** deployment parameters for the new version. If usefactory is set to true, and no salt is provided, `bytes32(0)` will be used by default. + - **"v5":** deployment parameters for the new version. If factory address is set, and no salt is provided, `bytes32(0)` will be used by default. If you want to deploy the iExec PoCo V5 smart contracts on a new blockchain, the recommended process is to: diff --git a/config/config.json b/config/config.json index 6eb44e000..8c85168ca 100644 --- a/config/config.json +++ b/config/config.json @@ -44,7 +44,6 @@ "WorkerpoolRegistry": "0xc398052563469e6Ea7C442aBf124aADE7ec2CC92" }, "v5": { - "usefactory": true, "salt": "0x0000000000000000000000000000000000000000000000000000000000000000", "AppRegistry": "0xB1C52075b276f87b1834919167312221d50c9D16", "DatasetRegistry": "0x799DAa22654128d0C64d5b79eac9283008158730", @@ -63,7 +62,6 @@ "WorkerpoolRegistry": null }, "v5": { - "usefactory": true, "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, @@ -79,7 +77,6 @@ "WorkerpoolRegistry": null }, "v5": { - "usefactory": true, "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, @@ -96,7 +93,6 @@ "WorkerpoolRegistry": "0xdAD30AAb14F569830bFd26EdF72df876dc30D20c" }, "v5": { - "usefactory": true, "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, @@ -112,7 +108,6 @@ "WorkerpoolRegistry": "0x3f4C18C322064576C048b1284b700288ffEf126B" }, "v5": { - "usefactory": true, "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, @@ -126,7 +121,6 @@ "WorkerpoolRegistry": "0x1Cae59C7745A61dD37CD17f174745959D0f3f400" }, "v5": { - "usefactory": true, "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, @@ -142,7 +136,43 @@ "v5": { "ERC1538Proxy": "0x3eca1B216A7DF1C7689aEb259fFB83ADFB894E7f", "IexecLibOrders_v5": "0xE8b04c85C47fcEc0e9eE30D4034e2997f6519123", - "usefactory": true, + "factory": "0xfAC000a12dA42B871c0AaD5F25391aAe62958Db1", + "factoryType": "generic", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "43113": { + "_comment": "Avalanche Fuji Testnet", + "asset": "Token", + "token": "0xb96484C8B0e27B08a86661a3c19A028f4d3e89ad", + "richman": "0xd88CF17D89533816E99C0427581aa8C72129037D", + "uniswap": false, + "v3": { + "Hub": null, + "AppRegistry": null, + "DatasetRegistry": null, + "WorkerpoolRegistry": null + }, + "v5": { + "factory": "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed", + "factoryType": "createx", + "ERC1538Proxy": "0x14B465079537655E1662F012e99EBa3863c8B9E0", + "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" + } + }, + "421614": { + "_comment": "Arbitrum Sepolia Testnet", + "asset": "Token", + "uniswap": false, + "v3": { + "Hub": null, + "AppRegistry": null, + "DatasetRegistry": null, + "WorkerpoolRegistry": null + }, + "v5": { + "factory": "0xba5Ed099633D3B313e4D5F7bdc1305d3c28ba5Ed", + "factoryType": "createx", "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" } }, @@ -158,7 +188,6 @@ "WorkerpoolRegistry": null }, "v5": { - "usefactory": true, "salt": "0x0000000000000000000000000000000000000000000000000000000000000000" } } diff --git a/config/config_native.json b/config/config_native.json index 6b31e5d38..6e65aec90 100644 --- a/config/config_native.json +++ b/config/config_native.json @@ -43,7 +43,6 @@ "WorkerpoolRegistry": null }, "v5": { - "usefactory": true, "salt": "0xbe1a000000000000000000000000000000000000000000000000000000000000" } } diff --git a/config/config_token.json b/config/config_token.json index a886c8266..8411bfc42 100644 --- a/config/config_token.json +++ b/config/config_token.json @@ -43,7 +43,6 @@ "WorkerpoolRegistry": null }, "v5": { - "usefactory": true, "salt": "0xbe1a000000000000000000000000000000000000000000000000000000000000" } } diff --git a/deploy/0_deploy.ts b/deploy/0_deploy.ts index 93a871267..f3ba03857 100644 --- a/deploy/0_deploy.ts +++ b/deploy/0_deploy.ts @@ -3,7 +3,7 @@ import { SignerWithAddress } from '@nomicfoundation/hardhat-ethers/signers'; import { ZeroAddress, ZeroHash } from 'ethers'; -import { ethers } from 'hardhat'; +import { deployments, ethers } from 'hardhat'; import { AppRegistry__factory, DatasetRegistry__factory, @@ -37,8 +37,8 @@ import { WorkerpoolRegistry__factory, } from '../typechain'; import { Ownable__factory } from '../typechain/factories/@openzeppelin/contracts/access'; -import config from '../utils/config'; import { FactoryDeployer } from '../utils/FactoryDeployer'; +import config from '../utils/config'; import { linkContractToProxy } from '../utils/proxy-tools'; /** @@ -56,8 +56,7 @@ export default async function deploy() { const chainId = (await ethers.provider.getNetwork()).chainId; const [owner] = await ethers.getSigners(); const deploymentOptions = config.getChainConfigOrDefault(chainId); - const salt = process.env.SALT || deploymentOptions.v5.salt || ethers.ZeroHash; - const factoryDeployer = new FactoryDeployer(owner, salt); + const factoryDeployer = new FactoryDeployer(owner, chainId); // Deploy RLC const isTokenMode = !config.isNativeChain(deploymentOptions); let rlcInstanceAddress = isTokenMode @@ -65,7 +64,7 @@ export default async function deploy() { : ZeroAddress; // native console.log(`RLC: ${rlcInstanceAddress}`); // Deploy ERC1538 proxy contracts - const erc1538UpdateAddress = await factoryDeployer.deployWithFactory( + const erc1538UpdateAddress = await factoryDeployer.deployContract( new ERC1538UpdateDelegate__factory(), ); const transferOwnershipCall = await Ownable__factory.connect( @@ -77,7 +76,7 @@ export default async function deploy() { .catch(() => { throw new Error('Failed to prepare transferOwnership data'); }); - const erc1538ProxyAddress = await factoryDeployer.deployWithFactory( + const erc1538ProxyAddress = await factoryDeployer.deployContract( new ERC1538Proxy__factory(), [erc1538UpdateAddress], transferOwnershipCall, @@ -85,7 +84,7 @@ export default async function deploy() { const erc1538: ERC1538Update = ERC1538Update__factory.connect(erc1538ProxyAddress, owner); console.log(`IexecInstance found at address: ${await erc1538.getAddress()}`); // Deploy library & modules - const iexecLibOrdersAddress = await factoryDeployer.deployWithFactory( + const iexecLibOrdersAddress = await factoryDeployer.deployContract( new IexecLibOrders_v5__factory(), ); const iexecLibOrders = { @@ -112,7 +111,7 @@ export default async function deploy() { new IexecPocoBoostAccessorsDelegate__factory(), ]; for (const module of modules) { - const address = await factoryDeployer.deployWithFactory(module); + const address = await factoryDeployer.deployContract(module); await linkContractToProxy(erc1538, address, module); } // Verify linking on ERC1538Proxy @@ -126,17 +125,17 @@ export default async function deploy() { const [method, , contract] = await erc1538QueryInstance.functionByIndex(i); console.log(`[${i}] ${contract} ${method}`); } - const appRegistryAddress = await factoryDeployer.deployWithFactory( + const appRegistryAddress = await factoryDeployer.deployContract( new AppRegistry__factory(), [], transferOwnershipCall, ); - const datasetRegistryAddress = await factoryDeployer.deployWithFactory( + const datasetRegistryAddress = await factoryDeployer.deployContract( new DatasetRegistry__factory(), [], transferOwnershipCall, ); - const workerpoolRegistryAddress = await factoryDeployer.deployWithFactory( + const workerpoolRegistryAddress = await factoryDeployer.deployContract( new WorkerpoolRegistry__factory(), [], transferOwnershipCall, @@ -213,11 +212,26 @@ export default async function deploy() { } async function getOrDeployRlc(token: string, owner: SignerWithAddress) { - return token // token - ? token - : await new RLC__factory() - .connect(owner) - .deploy() - .then((contract) => contract.waitForDeployment()) - .then((contract) => contract.getAddress()); + const rlcFactory = new RLC__factory().connect(owner); + let rlcAddress: string; + + if (token) { + console.log(`Using existing RLC token at: ${token}`); + rlcAddress = token; + } else { + console.log('Deploying new RLC token...'); + rlcAddress = await rlcFactory + .deploy() + .then((contract) => contract.waitForDeployment()) + .then((contract) => contract.getAddress()); + console.log(`New RLC token deployed at: ${rlcAddress}`); + } + + await deployments.save('RLC', { + abi: (rlcFactory as any).constructor.abi, + address: rlcAddress, + bytecode: (await rlcFactory.getDeployTransaction()).data, + deployedBytecode: await ethers.provider.getCode(rlcAddress), + }); + return rlcAddress; } diff --git a/hardhat.config.ts b/hardhat.config.ts index 24e368b23..3364a8bf6 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -13,6 +13,8 @@ import chainConfig from './utils/config'; const isNativeChainType = chainConfig.isNativeChain(); const isLocalFork = process.env.LOCAL_FORK == 'true'; +const isFujiFork = process.env.FUJI_FORK == 'true'; +const isArbitrumSepoliaFork = process.env.ARBITRUM_SEPOLIA_FORK == 'true'; const bellecourBlockscoutUrl = 'https://blockscout.bellecour.iex.ec'; /** @@ -28,6 +30,20 @@ const bellecourBaseConfig = { blockGasLimit: 6_700_000, }; +// Avalanche Fuji specific configuration +const fujiBaseConfig = { + gasPrice: 25_000_000_000, // 25 Gwei default + blockGasLimit: 8_000_000, + chainId: 43113, +}; + +// Arbitrum Sepolia specific configuration +const arbitrumSepoliaBaseConfig = { + gasPrice: 100_000_000, // 0.1 Gwei default (Arbitrum has lower gas prices) + blockGasLimit: 30_000_000, // Arbitrum has higher block gas limits + chainId: 421614, +}; + const settings = { optimizer: { enabled: true, @@ -81,6 +97,26 @@ const config: HardhatUserConfig = { }, chainId: 134, }), + ...(isFujiFork && { + forking: { + url: process.env.FUJI_RPC_URL || 'https://api.avax-test.network/ext/bc/C/rpc', + blockNumber: process.env.FUJI_BLOCK_NUMBER + ? parseInt(process.env.FUJI_BLOCK_NUMBER) + : undefined, + }, + ...fujiBaseConfig, + }), + ...(isArbitrumSepoliaFork && { + forking: { + url: + process.env.ARBITRUM_SEPOLIA_RPC_URL || + 'https://sepolia-rollup.arbitrum.io/rpc', + blockNumber: process.env.ARBITRUM_SEPOLIA_BLOCK_NUMBER + ? parseInt(process.env.ARBITRUM_SEPOLIA_BLOCK_NUMBER) + : undefined, + }, + ...arbitrumSepoliaBaseConfig, + }), }, 'external-hardhat': { ...defaultHardhatNetworkParams, @@ -93,6 +129,14 @@ const config: HardhatUserConfig = { accounts: 'remote', // Override defaults accounts for impersonation chainId: 134, }), + ...(isFujiFork && { + accounts: 'remote', // Override defaults accounts for impersonation + ...fujiBaseConfig, + }), + ...(isArbitrumSepoliaFork && { + accounts: 'remote', // Override defaults accounts for impersonation + ...arbitrumSepoliaBaseConfig, + }), }, 'dev-native': { chainId: 65535, @@ -128,6 +172,22 @@ const config: HardhatUserConfig = { mnemonic: process.env.MNEMONIC || '', }, }, + // Add Fuji as a network + avalancheFujiTestnet: { + url: process.env.FUJI_RPC_URL || 'https://api.avax-test.network/ext/bc/C/rpc', + accounts: { + mnemonic: process.env.MNEMONIC || HARDHAT_NETWORK_MNEMONIC, + }, + ...fujiBaseConfig, + }, + // Add Arbitrum Sepolia as a network + 'arbitrum-sepolia': { + url: process.env.ARBITRUM_SEPOLIA_RPC_URL || 'https://sepolia-rollup.arbitrum.io/rpc', + accounts: { + mnemonic: process.env.MNEMONIC || HARDHAT_NETWORK_MNEMONIC, + }, + ...arbitrumSepoliaBaseConfig, + }, viviani: { chainId: 133, url: 'https://viviani.iex.ec', @@ -155,6 +215,8 @@ const config: HardhatUserConfig = { etherscan: { apiKey: { mainnet: process.env.ETHERSCAN_API_KEY || '', + avalancheFujiTestnet: 'nothing', // a non-empty string is needed by the plugin. + arbitrumSepolia: process.env.ARBISCAN_API_KEY || '', viviani: 'nothing', // a non-empty string is needed by the plugin. bellecour: 'nothing', // a non-empty string is needed by the plugin. }, @@ -195,6 +257,7 @@ const config: HardhatUserConfig = { '@openzeppelin/contracts-v5/interfaces/IERC1271.sol', // Used in deployment '@amxx/factory/contracts/v6/GenericFactory.sol', + 'createx/src/ICreateX.sol', ], keep: true, // Slither requires compiled dependencies }, @@ -218,7 +281,7 @@ const config: HardhatUserConfig = { 'Store.v8.sol', ], }, - mocha: { timeout: 50000 }, + mocha: { timeout: 300000 }, }; /** diff --git a/package-lock.json b/package-lock.json index e36e6726f..433bb6956 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@openzeppelin/contracts": "3.3.0", "@openzeppelin/contracts-v5": "npm:@openzeppelin/contracts@^5.0.2", "@uniswap/v2-periphery": "1.1.0-beta.0", + "createx": "github:pcaversaccio/createx#v1.0.0", "rlc-faucet-contract": "1.0.10" }, "devDependencies": { @@ -4141,6 +4142,11 @@ "dev": true, "peer": true }, + "node_modules/createx": { + "version": "1.0.0", + "resolved": "git+ssh://git@github.com/pcaversaccio/createx.git#cbac803268835138f86a69bfe01fcf05a50e0447", + "license": "AGPL-3.0-only" + }, "node_modules/cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", @@ -15394,6 +15400,10 @@ "dev": true, "peer": true }, + "createx": { + "version": "git+ssh://git@github.com/pcaversaccio/createx.git#cbac803268835138f86a69bfe01fcf05a50e0447", + "from": "createx@github:pcaversaccio/createx#v1.0.0" + }, "cross-fetch": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", diff --git a/package.json b/package.json index 4dd8acfa0..bdb24a231 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,11 @@ "deploy": "npx hardhat deploy", "deploy:timelock": "hardhat run scripts/deploy-timelock.ts", "check-storage-layout": "npx hardhat run scripts/check-storage.ts", - "test": "REPORT_GAS=true npx hardhat test", - "test:native": "TEST__IS_NATIVE_CHAIN=true npx hardhat test test/byContract/IexecEscrow/IexecEscrowNative.test.ts", + "clean": "rm -rf deployments/hardhat", + "test": "npm run clean && REPORT_GAS=true npx hardhat test", + "test:arbitrum-sepolia": "npm run clean && ARBITRUM_SEPOLIA_FORK=true npx hardhat test", + "test:fuji": "npm run clean && FUJI_FORK=true npx hardhat test", + "test:native": "npm run clean && TEST__IS_NATIVE_CHAIN=true npx hardhat test test/byContract/IexecEscrow/IexecEscrowNative.test.ts", "coverage": "npx hardhat coverage", "verify": "npx hardhat verify", "format": "npx prettier --write", @@ -50,6 +53,7 @@ "@openzeppelin/contracts": "3.3.0", "@openzeppelin/contracts-v5": "npm:@openzeppelin/contracts@^5.0.2", "@uniswap/v2-periphery": "1.1.0-beta.0", + "createx": "github:pcaversaccio/createx#v1.0.0", "rlc-faucet-contract": "1.0.10" }, "devDependencies": { @@ -77,4 +81,4 @@ "sol2uml": "After 2.5.19, see https://github.com/naddison36/sol2uml/issues/183", "solidity-coverage": "Fixing Hardhat's solidity-coverage: TypeError: provider.send is not a function" } -} \ No newline at end of file +} diff --git a/scripts/deploy-timelock.ts b/scripts/deploy-timelock.ts index 9fad81e83..a1b519a8c 100644 --- a/scripts/deploy-timelock.ts +++ b/scripts/deploy-timelock.ts @@ -5,7 +5,6 @@ import { duration } from '@nomicfoundation/hardhat-network-helpers/dist/src/help import { ethers } from 'hardhat'; import { TimelockController__factory } from '../typechain'; import { FactoryDeployer } from '../utils/FactoryDeployer'; -import config from '../utils/config'; /** * Deploy TimelockController contract using the generic factory. @@ -15,10 +14,7 @@ export const deploy = async () => { console.log('Deploying TimelockController..'); const chainId = (await ethers.provider.getNetwork()).chainId; const [owner] = await ethers.getSigners(); - const salt = process.env.SALT || config.getChainConfigOrDefault(chainId).v5.salt; - - // Initialize factory deployer - const factoryDeployer = new FactoryDeployer(owner, salt); + const factoryDeployer = new FactoryDeployer(owner, chainId); // Deploy TimelockController const ONE_WEEK_IN_SECONDS = duration.days(7); @@ -38,7 +34,7 @@ export const deploy = async () => { ]; const constructorArgs = [ONE_WEEK_IN_SECONDS, ADMINISTRATORS, PROPOSERS, EXECUTORS]; const timelockFactory = new TimelockController__factory(owner); - await factoryDeployer.deployWithFactory(timelockFactory, constructorArgs); + await factoryDeployer.deployContract(timelockFactory, constructorArgs); }; if (require.main === module) { diff --git a/test/byContract/IexecAccessors/IexecAccessors.test.ts b/test/byContract/IexecAccessors/IexecAccessors.test.ts index c97a208ab..c1843ac9b 100644 --- a/test/byContract/IexecAccessors/IexecAccessors.test.ts +++ b/test/byContract/IexecAccessors/IexecAccessors.test.ts @@ -124,7 +124,7 @@ describe('IexecAccessors', async () => { // TODO test the case where token() == 0x0 in native mode. it('token', async function () { - expect(await iexecPoco.token()).to.equal('0x5FbDB2315678afecb367f032d93F642f64180aa3'); + expect(await iexecPoco.token()).to.equal((await deployments.get('RLC')).address); }); it('viewDeal', async function () { diff --git a/test/utils/hardhat-fixture-deployer.ts b/test/utils/hardhat-fixture-deployer.ts index c3ab4657c..329af58df 100644 --- a/test/utils/hardhat-fixture-deployer.ts +++ b/test/utils/hardhat-fixture-deployer.ts @@ -6,7 +6,7 @@ import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'; import { deployments, ethers } from 'hardhat'; import deploy from '../../deploy/0_deploy'; import deployEns from '../../deploy/1_deploy-ens'; -import { IexecInterfaceNative__factory } from '../../typechain'; +import { IexecInterfaceNative__factory, IexecInterfaceToken__factory } from '../../typechain'; import config from '../../utils/config'; import { getIexecAccounts } from '../../utils/poco-tools'; @@ -16,31 +16,9 @@ async function deployAll() { return (await deployments.get('ERC1538Proxy')).address; } -async function setUpLocalFork() { - const chainId = (await ethers.provider.getNetwork()).chainId; - const proxyAddress = config.getChainConfig(chainId).v5.ERC1538Proxy; - if (!proxyAddress) { - throw new Error('ERC1538Proxy is required'); - } - // Send RLCs to default accounts - const srlcRichSigner = await ethers.getImpersonatedSigner(proxyAddress); - const otherAccountInitAmount = - 10 * // Give this much RLC per account - 10 ** 9; +async function transferProxyOwnership(proxyAddress: string) { const accounts = await getIexecAccounts(); - const accountsArray = Object.values(accounts) as SignerWithAddress[]; - console.log(`Rich account ${srlcRichSigner.address} sending RLCs to other accounts..`); const iexecPoco = IexecInterfaceNative__factory.connect(proxyAddress, ethers.provider); - for (let i = 0; i < accountsArray.length; i++) { - const account = accountsArray[i]; - await iexecPoco - .connect(srlcRichSigner) - .transfer(account.address, otherAccountInitAmount) - .then((tx) => tx.wait()); - const balance = await iexecPoco.balanceOf(account.address); - console.log(`Account #${i}: ${account.address} (${balance.toLocaleString()} nRLC)`); - } - // Transfer ownership from Timelock to iexecAdmin EOA account const timelockAddress = await iexecPoco.owner(); const timelock = await ethers.getImpersonatedSigner(timelockAddress); const newIexecAdminAddress = accounts.iexecAdmin.address; @@ -51,12 +29,81 @@ async function setUpLocalFork() { .connect(timelock) .transferOwnership(newIexecAdminAddress) .then((tx) => tx.wait()); +} + +async function fundAccounts(tokenAddress: string, richmanAddress: string, isNativeMode: boolean) { + const accounts = await getIexecAccounts(); + const otherAccountInitAmount = isNativeMode ? 10 * 10 ** 9 : 10000 * 10 ** 9; + const accountsArray = Object.values(accounts) as SignerWithAddress[]; + if (!isNativeMode) { + await ethers.provider.send('hardhat_setBalance', [ + richmanAddress, + '0x1000000000000000000', // 1 ETH + ]); + } + console.log(`Rich account ${richmanAddress} sending RLCs to other accounts..`); + const richmanSigner = await ethers.getImpersonatedSigner(richmanAddress); + const tokenContract = isNativeMode + ? IexecInterfaceNative__factory.connect(tokenAddress, ethers.provider) + : IexecInterfaceToken__factory.connect(tokenAddress, ethers.provider); + for (let i = 0; i < accountsArray.length; i++) { + const account = accountsArray[i]; + await tokenContract + .connect(richmanSigner) + .transfer(account.address, otherAccountInitAmount) + .then((tx) => tx.wait()); + + const balance = await tokenContract.balanceOf(account.address); + console.log(`Account #${i}: ${account.address} (${balance.toLocaleString()} nRLC)`); + } +} + +async function setUpLocalForkInNativeMode() { + const chainId = (await ethers.provider.getNetwork()).chainId; + const proxyAddress = config.getChainConfig(chainId).v5.ERC1538Proxy; + if (!proxyAddress) { + throw new Error('ERC1538Proxy is required'); + } + await fundAccounts(proxyAddress, proxyAddress, true); + await transferProxyOwnership(proxyAddress); + return proxyAddress; } +async function setUpLocalForkInTokenMode() { + const chainId = (await ethers.provider.getNetwork()).chainId; + const chainConfig = config.getChainConfig(chainId); + const rlcTokenAddress = chainConfig.token; + const richmanAddress = chainConfig.richman; + if (rlcTokenAddress && richmanAddress) { + await fundAccounts(rlcTokenAddress, richmanAddress, false); + } + + const proxyAddress = chainConfig.v5.ERC1538Proxy; + if (proxyAddress) { + console.log(`Using existing ERC1538Proxy at ${proxyAddress}`); + await transferProxyOwnership(proxyAddress); + return proxyAddress; + } else { + console.log('No existing ERC1538Proxy found, deploying new contracts'); + // Deploy all contracts + await deploy(); + await deployEns(); + const newProxyAddress = (await deployments.get('ERC1538Proxy')).address; + console.log(`Deployed new ERC1538Proxy at ${newProxyAddress}`); + return newProxyAddress; + } +} + /** * @returns proxy address. */ export const loadHardhatFixtureDeployment = async () => { - return await loadFixture(process.env.LOCAL_FORK != 'true' ? deployAll : setUpLocalFork); + if (process.env.LOCAL_FORK == 'true') { + return await loadFixture(setUpLocalForkInNativeMode); + } + if (process.env.FUJI_FORK == 'true' || process.env.ARBITRUM_SEPOLIA_FORK == 'true') { + return await loadFixture(setUpLocalForkInTokenMode); + } + return await loadFixture(deployAll); }; diff --git a/utils/FactoryDeployer.ts b/utils/FactoryDeployer.ts index 4eda5e3c4..5a3b28afc 100644 --- a/utils/FactoryDeployer.ts +++ b/utils/FactoryDeployer.ts @@ -1,101 +1,219 @@ // SPDX-FileCopyrightText: 2020-2025 IEXEC BLOCKCHAIN TECH // SPDX-License-Identifier: Apache-2.0 -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 factorySignedTxJson from 'createx/scripts/presigned-createx-deployment-transactions/signed_serialised_transaction_gaslimit_3000000_.json'; import { ContractFactory } from 'ethers'; -import hre, { deployments, ethers } from 'hardhat'; -import { GenericFactory, GenericFactory__factory } from '../typechain'; -import config from './config'; +import { deployments, ethers } from 'hardhat'; +import { GenericFactory, GenericFactory__factory, ICreateX, ICreateX__factory } from '../typechain'; +import config from '../utils/config'; import { getBaseNameFromContractFactory } from './deploy-tools'; - export class FactoryDeployer { owner: SignerWithAddress; salt: string; - genericFactory!: GenericFactory; + factoryAddress?: string; + factoryType: string; + factory?: ICreateX | GenericFactory; - constructor(owner: SignerWithAddress, salt: string) { + constructor(owner: SignerWithAddress, chainId: bigint) { + const deploymentOptions = config.getChainConfigOrDefault(chainId); this.owner = owner; - this.salt = salt; + this.salt = process.env.SALT || deploymentOptions.v5.salt || ethers.ZeroHash; + this.factoryAddress = process.env.FACTORY_ADDRESS || deploymentOptions.v5.factory; + this.factoryType = + process.env.FACTORY_TYPE || deploymentOptions.v5.factoryType || 'createx'; + } + + /** + * Deploy a contract through the configured factory [and optionally trigger a call] + */ + async deployContract( + contractFactory: ContractFactory, + constructorArgs?: any[], + call?: string, + ): Promise { + await this.initFactory(); + if (this.factoryType === 'createx') { + return await this.deployWithCreateX(contractFactory, constructorArgs, call); + } + return await this.deployWithGenericFactory(contractFactory, constructorArgs, call); + } + + /** + * Deploy a contract through CreateX [and optionally trigger a call] + */ + private async deployWithCreateX( + contractFactory: ContractFactory, + constructorArgs?: any[], + call?: string, + ): Promise { + const createX = this.factory as ICreateX; + let bytecode = (await contractFactory.getDeployTransaction(...(constructorArgs ?? []))) + .data; + if (!bytecode) { + throw new Error('Failed to prepare bytecode'); + } + const initCodeHash = ethers.keccak256(bytecode); + const saltHash = ethers.keccak256(this.salt); + const contractAddress = await createX['computeCreate2Address(bytes32,bytes32)']( + saltHash, + initCodeHash, + ); + console.log(`Deploying at ${contractAddress}`); + const previouslyDeployed = (await ethers.provider.getCode(contractAddress)) !== '0x'; + if (!previouslyDeployed) { + await ( + call + ? createX['deployCreate2AndInit(bytes32,bytes,bytes,(uint256,uint256))']( + this.salt, + bytecode, + call, + { + constructorAmount: 0, + initCallAmount: 0, + }, + ) + : createX['deployCreate2(bytes32,bytes)'](this.salt, bytecode) + ).then((tx) => tx.wait()); + } + await this.saveDeployment( + contractFactory, + contractAddress, + bytecode, + constructorArgs, + previouslyDeployed, + ); + return contractAddress; } /** * Deploy a contract through GenericFactory [and optionally trigger a call] */ - async deployWithFactory( + private async deployWithGenericFactory( contractFactory: ContractFactory, constructorArgs?: any[], call?: string, - ) { - await this.init(); + ): Promise { + const genericFactory = this.factory as GenericFactory; 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)); + ? genericFactory.predictAddressWithCall(bytecode, this.salt, call) + : 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) + ? genericFactory.createContractAndCall(bytecode, this.salt, call) + : genericFactory.createContract(bytecode, this.salt) ).then((tx) => tx.wait()); } + await this.saveDeployment( + contractFactory, + contractAddress, + bytecode, + constructorArgs, + previouslyDeployed, + ); + return contractAddress; + } + + /** + * Save deployment information to Hardhat deployments + */ + private async saveDeployment( + contractFactory: ContractFactory, + address: string, + bytecode: string, + constructorArgs?: any[], + previouslyDeployed: boolean = false, + ): Promise { const contractName = getBaseNameFromContractFactory(contractFactory); console.log( - `${contractName}: ${contractAddress} ${ - previouslyDeployed ? ' (previously deployed)' : '' - }`, + `${contractName}: ${address} ${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, + address: address, bytecode: bytecode, args: constructorArgs, }); - return contractAddress; } - private async init() { - if (this.genericFactory) { - // Already initialized. - return; + /** + * Initialize the appropriate factory based on factoryType + */ + private async initFactory() { + if (this.factory) { + return; // Factory already initialized } - 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; + if (this.factoryType === 'createx') { + await this.initCreateX(); + } else { + await this.initGenericFactory(); } - 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'); + } + + /** + * Initialize Generic Factory + */ + private async initGenericFactory() { + if (!this.factoryAddress) { + throw new Error('Factory address not set for GenericFactory'); + } + this.factory = GenericFactory__factory.connect(this.factoryAddress, this.owner); + if ((await ethers.provider.getCode(this.factoryAddress)) !== '0x') { + console.log(`→ GenericFactory is available on this network at ${this.factoryAddress}`); + return; } + throw new Error('GenericFactory not deployed at the provided address'); } -} -interface FactoryConfig { - address: string; - deployer: string; - cost: string; - tx: string; - abi: any[]; + /** + * Initialize CreateX Factory + */ + private async initCreateX() { + if (!this.factoryAddress) { + try { + console.log(`→ CreateX is not yet deployed on this network`); + const factorySignedTx = ethers.Transaction.from(factorySignedTxJson); + const deployer = factorySignedTx.from; + const cost = (factorySignedTx.gasPrice! * factorySignedTx.gasLimit!).toString(); + const tx = factorySignedTxJson; + + await this.owner + .sendTransaction({ + to: deployer, + value: cost, + }) + .then((tx) => tx.wait()); + await ethers.provider.broadcastTransaction(tx).then((tx) => tx.wait()); + + // Calculate the deployed contract address + const createdContractAddress = ethers.getCreateAddress({ + from: factorySignedTx.from!, + nonce: factorySignedTx.nonce, + }); + console.log( + `→ CreateX successfully deployed at address: ${createdContractAddress}`, + ); + this.factoryAddress = createdContractAddress; + this.factory = ICreateX__factory.connect(this.factoryAddress, this.owner); + } catch (e) { + console.log(e); + throw new Error('→ Error deploying CreateX'); + } + } else { + this.factory = ICreateX__factory.connect(this.factoryAddress, this.owner); + if ((await ethers.provider.getCode(this.factoryAddress)) !== '0x') { + console.log(`→ CreateX is available on this network at ${this.factoryAddress}`); + return; + } + throw new Error('CreateX not deployed at the provided address'); + } + } } diff --git a/utils/config.ts b/utils/config.ts index 7cdb9859a..d7f7ffd68 100644 --- a/utils/config.ts +++ b/utils/config.ts @@ -1,5 +1,5 @@ -import { Category } from './poco-tools'; import json from '../config/config.json'; +import { Category } from './poco-tools'; const config = json as Config; @@ -38,7 +38,8 @@ type Config = { type ChainConfig = { _comment: string; asset: string; - token?: string | null; + token?: string | null; // The token deployed should be compatible with Approve and call + richman?: string | null; // The richman account is needed if the token is already deployed uniswap?: boolean; etoken?: string; v3: { @@ -48,8 +49,9 @@ type ChainConfig = { WorkerpoolRegistry: string | null; }; v5: { - usefactory: boolean; - salt: string; + factory?: string; + factoryType?: string; + salt?: string; AppRegistry?: string; DatasetRegistry?: string; WorkerpoolRegistry?: string;