diff --git a/hardhat.config.ts b/hardhat.config.ts index 820d44c1..0aba7adb 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -19,7 +19,7 @@ const { network } = yargs dotenv.config(); const { INFURA_KEY, MNEMONIC, ETHERSCAN_API_KEY, PK } = process.env; -import "./src/factory/singleton-deployment"; +import "./src/tasks/singleton-deployment"; const DEFAULT_MNEMONIC = "candy maple cake sugar pudding cream honey rich smooth crumble sweet treat"; diff --git a/src/abi/oz_governor.ts b/src/abi/oz_governor.ts new file mode 100644 index 00000000..a6ab6742 --- /dev/null +++ b/src/abi/oz_governor.ts @@ -0,0 +1,688 @@ +export default [ + { + inputs: [ + { internalType: "address", name: "_owner", type: "address" }, + { internalType: "address", name: "_target", type: "address" }, + { internalType: "address", name: "_multisend", type: "address" }, + { internalType: "address", name: "_token", type: "address" }, + { internalType: "string", name: "_name", type: "string" }, + { internalType: "uint256", name: "_votingDelay", type: "uint256" }, + { internalType: "uint256", name: "_votingPeriod", type: "uint256" }, + { + internalType: "uint256", + name: "_proposalThreshold", + type: "uint256", + }, + { internalType: "uint256", name: "_quorum", type: "uint256" }, + { + internalType: "uint64", + name: "_initialVoteExtension", + type: "uint64", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { inputs: [], name: "TransactionsFailed", type: "error" }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint64", + name: "oldVoteExtension", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "newVoteExtension", + type: "uint64", + }, + ], + name: "LateQuorumVoteExtensionSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "multisend", + type: "address", + }, + ], + name: "MultisendSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "target", + type: "address", + }, + ], + name: "OZGovernorModuleSetUp", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousOwner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + ], + name: "ProposalCanceled", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: false, + internalType: "address", + name: "proposer", + type: "address", + }, + { + indexed: false, + internalType: "address[]", + name: "targets", + type: "address[]", + }, + { + indexed: false, + internalType: "uint256[]", + name: "values", + type: "uint256[]", + }, + { + indexed: false, + internalType: "string[]", + name: "signatures", + type: "string[]", + }, + { + indexed: false, + internalType: "bytes[]", + name: "calldatas", + type: "bytes[]", + }, + { + indexed: false, + internalType: "uint256", + name: "startBlock", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "endBlock", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "description", + type: "string", + }, + ], + name: "ProposalCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + ], + name: "ProposalExecuted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: false, + internalType: "uint64", + name: "extendedDeadline", + type: "uint64", + }, + ], + name: "ProposalExtended", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "oldProposalThreshold", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "newProposalThreshold", + type: "uint256", + }, + ], + name: "ProposalThresholdSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "oldQuorumNumerator", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "newQuorumNumerator", + type: "uint256", + }, + ], + name: "QuorumNumeratorUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "previousTarget", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "newTarget", + type: "address", + }, + ], + name: "TargetSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "voter", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: false, + internalType: "uint8", + name: "support", + type: "uint8", + }, + { + indexed: false, + internalType: "uint256", + name: "weight", + type: "uint256", + }, + { + indexed: false, + internalType: "string", + name: "reason", + type: "string", + }, + ], + name: "VoteCast", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "oldVotingDelay", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "newVotingDelay", + type: "uint256", + }, + ], + name: "VotingDelaySet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint256", + name: "oldVotingPeriod", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "newVotingPeriod", + type: "uint256", + }, + ], + name: "VotingPeriodSet", + type: "event", + }, + { + inputs: [], + name: "BALLOT_TYPEHASH", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "COUNTING_MODE", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "pure", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "proposalId", type: "uint256" }, + { internalType: "uint8", name: "support", type: "uint8" }, + ], + name: "castVote", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "proposalId", type: "uint256" }, + { internalType: "uint8", name: "support", type: "uint8" }, + { internalType: "uint8", name: "v", type: "uint8" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + name: "castVoteBySig", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "proposalId", type: "uint256" }, + { internalType: "uint8", name: "support", type: "uint8" }, + { internalType: "string", name: "reason", type: "string" }, + ], + name: "castVoteWithReason", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address[]", name: "targets", type: "address[]" }, + { internalType: "uint256[]", name: "values", type: "uint256[]" }, + { internalType: "bytes[]", name: "calldatas", type: "bytes[]" }, + { + internalType: "bytes32", + name: "descriptionHash", + type: "bytes32", + }, + ], + name: "execute", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "account", type: "address" }, + { internalType: "uint256", name: "blockNumber", type: "uint256" }, + ], + name: "getVotes", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "proposalId", type: "uint256" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "hasVoted", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address[]", name: "targets", type: "address[]" }, + { internalType: "uint256[]", name: "values", type: "uint256[]" }, + { internalType: "bytes[]", name: "calldatas", type: "bytes[]" }, + { + internalType: "bytes32", + name: "descriptionHash", + type: "bytes32", + }, + ], + name: "hashProposal", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "lateQuorumVoteExtension", + outputs: [{ internalType: "uint64", name: "", type: "uint64" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "multisend", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "name", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "proposalId", type: "uint256" }], + name: "proposalDeadline", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "proposalId", type: "uint256" }], + name: "proposalSnapshot", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "proposalThreshold", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "proposalId", type: "uint256" }], + name: "proposalVotes", + outputs: [ + { internalType: "uint256", name: "againstVotes", type: "uint256" }, + { internalType: "uint256", name: "forVotes", type: "uint256" }, + { internalType: "uint256", name: "abstainVotes", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address[]", name: "targets", type: "address[]" }, + { internalType: "uint256[]", name: "values", type: "uint256[]" }, + { internalType: "bytes[]", name: "calldatas", type: "bytes[]" }, + { internalType: "string", name: "description", type: "string" }, + ], + name: "propose", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "blockNumber", type: "uint256" }], + name: "quorum", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "quorumDenominator", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "quorumNumerator", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "target", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "relay", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint64", name: "newVoteExtension", type: "uint64" }, + ], + name: "setLateQuorumVoteExtension", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "_multisend", type: "address" }], + name: "setMultisend", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "newProposalThreshold", + type: "uint256", + }, + ], + name: "setProposalThreshold", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "_target", type: "address" }], + name: "setTarget", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes", name: "initializeParams", type: "bytes" }, + ], + name: "setUp", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "newVotingDelay", type: "uint256" }, + ], + name: "setVotingDelay", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "newVotingPeriod", + type: "uint256", + }, + ], + name: "setVotingPeriod", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "proposalId", type: "uint256" }], + name: "state", + outputs: [ + { + internalType: "enum IGovernorUpgradeable.ProposalState", + name: "", + type: "uint8", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "target", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "token", + outputs: [ + { + internalType: "contract IVotesUpgradeable", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "_owner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "newQuorumNumerator", + type: "uint256", + }, + ], + name: "updateQuorumNumerator", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "version", + outputs: [{ internalType: "string", name: "", type: "string" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "votingDelay", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "votingPeriod", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { stateMutability: "payable", type: "receive" }, +]; diff --git a/src/factory/README.md b/src/factory/README.md index dd1ef784..681a7590 100644 --- a/src/factory/README.md +++ b/src/factory/README.md @@ -12,15 +12,15 @@ You can check the factory file to see more details, it consists of 5 methods, de This method is used to deploy contracts listed in `./constants.ts`. -- Interface: `deployAndSetUpModule(moduleName, args, provider, chainId, salt)` +- Interface: `deployAndSetUpModule(moduleName, setupArgs, provider, chainId, salt)` - Arguments: - `moduleName`: Name of the module to be deployed, note that it needs to exist as a key in the [CONTRACT_ADDRESSES](./constants.ts#L3-L12) object - - `args`: An object with two attributes: `value` and `types` + - `setupArgs`: An object with two attributes: `value` and `types` - In `value` it expects an array of the arguments of the `setUp` function of the module to deploy - In `types` it expects an array of the types of every value - `provider`: Ethereum provider, expects an instance of `JsonRpcProvider` from `ethers` - `chainId`: Number of network to interact with - - `salt`: For the Create2 op code + - `salt`: For the Create2 op code - Returns: An object with the transaction built in order to be executed by the Safe, and the expected address of the new module, this will allow developers to batch the transaction of deployment + enable module on safe. Example: ```json @@ -38,15 +38,16 @@ This method is used to deploy contracts listed in `./constants.ts`. This method is similar to `deployAndSetUpModule`, however, it deals with the deployment of contracts that is NOT listed in `./constants.ts`. -- Interface: `deployAndSetUpCustomModule(masterCopyAddress, abi, args, provider, chainId)` +- Interface: `deployAndSetUpCustomModule(mastercopyAddress, abi, setupArgs, provider, chainId, saltNonce)` - Arguments: - - `masterCopyAddress`: The address of the module to be deployed + - `mastercopyAddress`: The address of the module to be deployed - `abi`: The ABI of the module to be deployed - - `args`: An object with two attributes: `value` and `types` + - `setupArgs`: An object with two attributes: `value` and `types` - In `value` it expects an array of the arguments of the `setUp` function of the module to deploy - In `types` it expects an array of the types of every value - `provider`: Ethereum provider, expects an instance of `JsonRpcProvider` from `ethers` - `chainId`: Number of network to interact with + - `saltNonce`: Some salt to use for the deployment - Returns: An object with the transaction built in order to be executed by the Safe, and the expected address of the new module, this will allow developers to batch the transaction of deployment + enable module on safe. Example: ```json @@ -64,22 +65,23 @@ This method is similar to `deployAndSetUpModule`, however, it deals with the dep This method is used to calculate the resulting address of a deployed module given the provided parameters. It is useful for building multisend transactions that both deploy a module and then make calls to that module or calls referencing the module's address. -- Interface: `calculateProxyAddress(factory, masterCopy, initData)` +- Interface: `calculateProxyAddress(moduleFactory, mastercopyAddress, initData, saltNonce)` - Arguments: - - `factory`: Factory contract object of the Module Proxy Factory contract - - `masterCopy`: Address of the Master Copy of the Module + - `moduleFactory`: Module factory contract object of the Module Proxy Factory contract + - `mastercopyAddress`: Address of the Master Copy of the Module - `initData`: Encoded function data that is used to set up the module + - `saltNonce`: Some salt to use for the deployment - Returns: A string with the expected address ### 4. Get Module This method returns an instance of a given module. -- Interface: `getModuleInstance(moduleName, address, provider)` +- Interface: `getModuleInstance(moduleName, moduleAddress, provider)` - Arguments: - `moduleName`: Name of the module to be deployed, note that it needs to exist as a key in the [CONTRACT_ADDRESSES](./constants.ts#L3-L12) object - - `address`: Address of the Module contract + - `moduleAddress`: Address of the Module contract - `provider`: Ethereum provider, expects an instance of `JsonRpcProvider` from `ethers` - Returns: A Contract instance of the Module @@ -97,8 +99,8 @@ This method returns an object with the an instance of the factory contract and t ```json { - "factory": Contract, - "module": Contract, + "moduleFactory": Contract, + "moduleMastercopy": Contract, } ``` diff --git a/src/factory/constants.ts b/src/factory/constants.ts index 60a793f4..a50bcccb 100644 --- a/src/factory/constants.ts +++ b/src/factory/constants.ts @@ -1,3 +1,4 @@ +import OzGovernorAbi from "../abi/oz_governor"; import { KnownContracts } from "./types"; export enum SUPPORTED_NETWORKS { @@ -30,6 +31,7 @@ const MasterCopyAddresses: Record = { [KnownContracts.ROLES]: "0x85388a8cd772b19a468F982Dc264C238856939C9", // missing: mumbai, arbitrum, optimism tellor: "", optimisticGovernor: "", + [KnownContracts.OZ_GOVERNOR]: "", }; export const CONTRACT_ADDRESSES: Record< @@ -46,6 +48,7 @@ export const CONTRACT_ADDRESSES: Record< ...MasterCopyAddresses, [KnownContracts.OPTIMISTIC_GOVERNOR]: "0x1340229DCF6e0bed7D9c2356929987C2A720F836", + [KnownContracts.OZ_GOVERNOR]: "0x011Ad6A7FE4FB9226204dDBe2b6a5Fc109961dce", }, [SUPPORTED_NETWORKS.BinanceSmartChain]: { ...MasterCopyAddresses }, [SUPPORTED_NETWORKS.GnosisChain]: { ...MasterCopyAddresses }, @@ -65,7 +68,7 @@ export const CONTRACT_ADDRESSES: Record< [SUPPORTED_NETWORKS.Avalanche]: { ...MasterCopyAddresses }, //TODO: figure out what to change }; -export const CONTRACT_ABIS: Record = { +export const CONTRACT_ABIS: Record = { [KnownContracts.META_GUARD]: [ `function setUp(bytes memory initParams) public`, `function setAvatar(address _avatar) public`, @@ -262,4 +265,5 @@ export const CONTRACT_ABIS: Record = { "function transferOwnership(address newOwner)", "function unscopeParameter(uint16 role, address targetAddress, bytes4 functionSig, uint8 paramIndex)", ], + ozGovernor: OzGovernorAbi, }; diff --git a/src/factory/deploy_module_factory.ts b/src/factory/deploy_module_factory.ts new file mode 100644 index 00000000..cd5e29f7 --- /dev/null +++ b/src/factory/deploy_module_factory.ts @@ -0,0 +1,65 @@ +import "hardhat-deploy"; +import "@nomiclabs/hardhat-ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { getSingletonFactory } from "./singleton_factory"; + +const factorySalt = + "0xb0519c4c4b7945db302f69180b86f1a668153a476802c1c445fcb691ef23ef16"; +const AddressZero = "0x0000000000000000000000000000000000000000"; + +/** + * Deploy a module factory via the singleton factory. + * It will therefore get the same address on any chain. + * @param hre hardhat runtime environment + * @returns The address of the deployed module factory + */ +export const deployModuleFactory = async (hre: HardhatRuntimeEnvironment) => { + const singletonFactory = await getSingletonFactory(hre); + console.log("Singleton Factory: ", singletonFactory.address); + const Factory = await hre.ethers.getContractFactory("ModuleProxyFactory"); + // const singletonFactory = new hardhat.ethers.Contract(singletonFactoryAddress, singletonFactoryAbi) + + const targetAddress = await singletonFactory.callStatic.deploy( + Factory.bytecode, + factorySalt + ); + if (targetAddress == AddressZero) { + console.log( + "ModuleProxyFactory already deployed to target address on this network." + ); + return; + } else { + console.log("Target Factory Address:", targetAddress); + } + + const transactionResponse = await singletonFactory.deploy( + Factory.bytecode, + factorySalt + ); + + const result = await transactionResponse.wait(); + console.log("Deploy transaction: ", result.transactionHash); + + const factory = await hre.ethers.getContractAt( + "ModuleProxyFactory", + targetAddress + ); + + const factoryArtifact = await hre.artifacts.readArtifact( + "ModuleProxyFactory" + ); + + if ( + (await hre.ethers.provider.getCode(factory.address)) != + factoryArtifact.deployedBytecode + ) { + throw new Error( + "Deployment unsuccessful: deployed bytecode does not match." + ); + } else { + console.log( + "Successfully deployed ModuleProxyFactory to target address! 🎉" + ); + } + return targetAddress; +}; diff --git a/src/factory/factory.ts b/src/factory/factory.ts deleted file mode 100644 index 4ca68ab7..00000000 --- a/src/factory/factory.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { ethers, Contract, Signer, BigNumber } from "ethers"; -import { ABI } from "hardhat-deploy/dist/types"; - -import { - CONTRACT_ADDRESSES, - CONTRACT_ABIS, - SUPPORTED_NETWORKS, -} from "./constants"; -import { KnownContracts } from "./types"; - -export const deployAndSetUpModule = ( - contractName: KnownContracts, - args: { - types: Array; - values: Array; - }, - provider: ethers.providers.JsonRpcProvider, - chainId: number, - saltNonce: string -): { - transaction: { - data: string; - to: string; - value: ethers.BigNumber; - }; - expectedModuleAddress: string; -} => { - const { factory, module } = getFactoryAndMasterCopy( - contractName, - provider, - chainId - ); - return getDeployAndSetupTx(factory, module, args, saltNonce); -}; - -export const deployAndSetUpCustomModule = ( - masterCopyAddress: string, - abi: ABI, - setupArgs: { - types: Array; - values: Array; - }, - provider: ethers.providers.JsonRpcProvider, - chainId: number, - saltNonce: string -): { - transaction: { - data: string; - to: string; - value: ethers.BigNumber; - }; - expectedModuleAddress: string; -} => { - const chainContracts = CONTRACT_ADDRESSES[chainId as SUPPORTED_NETWORKS]; - const factoryAddress = chainContracts.factory; - const factory = new Contract(factoryAddress, CONTRACT_ABIS.factory, provider); - const module = new Contract(masterCopyAddress, abi, provider); - - return getDeployAndSetupTx(factory, module, setupArgs, saltNonce); -}; - -const getDeployAndSetupTx = ( - factory: ethers.Contract, - module: ethers.Contract, - args: { - types: Array; - values: Array; - }, - saltNonce: string -) => { - const encodedInitParams = ethers.utils.defaultAbiCoder.encode( - args.types, - args.values - ); - const moduleSetupData = module.interface.encodeFunctionData("setUp", [ - encodedInitParams, - ]); - - const expectedModuleAddress = calculateProxyAddress( - factory, - module.address, - moduleSetupData, - saltNonce - ); - - const deployData = factory.interface.encodeFunctionData("deployModule", [ - module.address, - moduleSetupData, - saltNonce, - ]); - const transaction = { - data: deployData, - to: factory.address, - value: BigNumber.from(0), - }; - return { - transaction, - expectedModuleAddress, - }; -}; - -export const calculateProxyAddress = ( - factory: Contract, - masterCopy: string, - initData: string, - saltNonce: string -): string => { - const masterCopyAddress = masterCopy.toLowerCase().replace(/^0x/, ""); - const byteCode = - "0x602d8060093d393df3363d3d373d3d3d363d73" + - masterCopyAddress + - "5af43d82803e903d91602b57fd5bf3"; - - const salt = ethers.utils.solidityKeccak256( - ["bytes32", "uint256"], - [ethers.utils.solidityKeccak256(["bytes"], [initData]), saltNonce] - ); - - return ethers.utils.getCreate2Address( - factory.address, - salt, - ethers.utils.keccak256(byteCode) - ); -}; - -export const getModuleInstance = ( - moduleName: KnownContracts, - address: string, - provider: ethers.providers.JsonRpcProvider | Signer -): ethers.Contract => { - const moduleIsNotSupported = !Object.keys(CONTRACT_ABIS).includes(moduleName); - if (moduleIsNotSupported) { - throw new Error("Module " + moduleName + " not supported"); - } - return new Contract(address, CONTRACT_ABIS[moduleName], provider); -}; - -export const getFactoryAndMasterCopy = ( - moduleName: KnownContracts, - provider: ethers.providers.JsonRpcProvider, - chainId: number -): { - factory: ethers.Contract; - module: ethers.Contract; -} => { - const chainContracts = CONTRACT_ADDRESSES[chainId as SUPPORTED_NETWORKS]; - const masterCopyAddress = chainContracts[moduleName]; - const factoryAddress = chainContracts.factory; - const module = getModuleInstance(moduleName, masterCopyAddress, provider); - const factory = new Contract(factoryAddress, CONTRACT_ABIS.factory, provider); - - return { - factory, - module, - }; -}; diff --git a/src/factory/index.ts b/src/factory/index.ts index fa1d97b9..abd87252 100644 --- a/src/factory/index.ts +++ b/src/factory/index.ts @@ -1,3 +1,4 @@ -export * from "./factory"; +export * from "./module_deployer"; +export * from "./mastercopy_deployer"; export * from "./types"; export * from "./constants"; diff --git a/src/factory/mastercopy_deployer.ts b/src/factory/mastercopy_deployer.ts new file mode 100644 index 00000000..06fac676 --- /dev/null +++ b/src/factory/mastercopy_deployer.ts @@ -0,0 +1,52 @@ +import { ContractFactory } from "ethers"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { getSingletonFactory } from "./singleton_factory"; + +const salt = + "0xb0519c4c4b7945db302f69180b86f1a668153a476802c1c445fcb691ef23ef16"; + +/** + * Deploy a module's mastercopy via the singleton factory. + * + * To get the same address on any chain. + * @param hre hardhat runtime environment + * @param mastercopyContractFactory + * @param args + * @returns The address of the deployed module mastercopy + */ +export const deployMastercopy = async ( + hre: HardhatRuntimeEnvironment, + mastercopyContractFactory: ContractFactory, + args: Array +) => { + const deploymentTx = mastercopyContractFactory.getDeployTransaction(...args); + + const singletonFactory = await getSingletonFactory(hre); + + const targetAddress = await singletonFactory.callStatic.deploy( + deploymentTx.data, + salt + ); + + if (targetAddress == "0x0000000000000000000000000000000000000000") { + throw new Error( + "Mastercopy already deployed to target address on this network." + ); + } + + console.log("targetAddress", targetAddress); + + const deployData = await singletonFactory.deploy(deploymentTx.data, salt, { + gasLimit: 10000000, + }); + + const recept = await deployData.wait(); + console.log("recept", recept); + + if ((await hre.ethers.provider.getCode(targetAddress)).length > 2) { + console.log( + "Successfully deployed ModuleProxyFactory to target address! 🎉" + ); + } + return targetAddress; +}; diff --git a/src/factory/module_deployer.ts b/src/factory/module_deployer.ts new file mode 100644 index 00000000..503b07f6 --- /dev/null +++ b/src/factory/module_deployer.ts @@ -0,0 +1,199 @@ +import { ethers, Contract, Signer, BigNumber } from "ethers"; +import { ABI } from "hardhat-deploy/dist/types"; + +import { + CONTRACT_ADDRESSES, + CONTRACT_ABIS, + SUPPORTED_NETWORKS, +} from "./constants"; +import { KnownContracts } from "./types"; + +type TxAndExpectedAddress = { + transaction: { + data: string; + to: string; + value: ethers.BigNumber; + }; + expectedModuleAddress: string; +}; + +/** + * Get the transaction for deploying a module proxy through the module factory. + * This will also initialize the module proxy by calling the setup function. + * + * @param moduleName Name of the module to deploy (must be present in `KnownContracts`) + * @param setupArgs The arguments for the setup function of the module + * @param provider + * @param chainId + * @param saltNonce + * @returns the transaction and the expected address of the module proxy + */ +export const deployAndSetUpModule = ( + moduleName: KnownContracts, + setupArgs: { + types: Array; + values: Array; + }, + provider: ethers.providers.JsonRpcProvider, + chainId: number, + saltNonce: string +): TxAndExpectedAddress => { + const { moduleFactory, moduleMastercopy } = getModuleFactoryAndMasterCopy( + moduleName, + provider, + chainId + ); + return getDeployAndSetupTx( + moduleFactory, + moduleMastercopy, + setupArgs, + saltNonce + ); +}; + +/** + * Get the transaction for deploying a module proxy through the module factory. + * This will also initialize the module proxy by calling the setup function. + * + * This method is for modules that do not have a mastercopy listed in the `KnownContracts` + * @param mastercopyAddress address of the mastercopy to use + * @param abi abi of the module + * @param setupArgs The arguments for the setup function of the module + * @param provider + * @param chainId + * @param saltNonce + * @returns the transaction and the expected address of the module proxy + */ +export const deployAndSetUpCustomModule = ( + mastercopyAddress: string, + abi: ABI, + setupArgs: { + types: Array; + values: Array; + }, + provider: ethers.providers.JsonRpcProvider, + chainId: number, + saltNonce: string +): TxAndExpectedAddress => { + const chainContracts = CONTRACT_ADDRESSES[chainId as SUPPORTED_NETWORKS]; + const moduleFactoryAddress = chainContracts.factory; + const moduleFactory = new Contract( + moduleFactoryAddress, + CONTRACT_ABIS.factory, + provider + ); + const moduleMastercopy = new Contract(mastercopyAddress, abi, provider); + + return getDeployAndSetupTx( + moduleFactory, + moduleMastercopy, + setupArgs, + saltNonce + ); +}; + +const getDeployAndSetupTx = ( + moduleFactory: ethers.Contract, + moduleMastercopy: ethers.Contract, + setupArgs: { + types: Array; + values: Array; + }, + saltNonce: string +) => { + const encodedInitParams = ethers.utils.defaultAbiCoder.encode( + setupArgs.types, + setupArgs.values + ); + const moduleSetupData = moduleMastercopy.interface.encodeFunctionData( + "setUp", + [encodedInitParams] + ); + + const expectedModuleAddress = calculateProxyAddress( + moduleFactory, + moduleMastercopy.address, + moduleSetupData, + saltNonce + ); + + const deployData = moduleFactory.interface.encodeFunctionData( + "deployModule", + [moduleMastercopy.address, moduleSetupData, saltNonce] + ); + const transaction = { + data: deployData, + to: moduleFactory.address, + value: BigNumber.from(0), + }; + return { + transaction, + expectedModuleAddress, + }; +}; + +export const calculateProxyAddress = ( + moduleFactory: Contract, + mastercopyAddress: string, + initData: string, + saltNonce: string +): string => { + const mastercopyAddressFormatted = mastercopyAddress + .toLowerCase() + .replace(/^0x/, ""); + const byteCode = + "0x602d8060093d393df3363d3d373d3d3d363d73" + + mastercopyAddressFormatted + + "5af43d82803e903d91602b57fd5bf3"; + + const salt = ethers.utils.solidityKeccak256( + ["bytes32", "uint256"], + [ethers.utils.solidityKeccak256(["bytes"], [initData]), saltNonce] + ); + + return ethers.utils.getCreate2Address( + moduleFactory.address, + salt, + ethers.utils.keccak256(byteCode) + ); +}; + +export const getModuleInstance = ( + moduleName: KnownContracts, + moduleAddress: string, + provider: ethers.providers.JsonRpcProvider | Signer +): ethers.Contract => { + const moduleIsNotSupported = !Object.keys(CONTRACT_ABIS).includes(moduleName); + if (moduleIsNotSupported) { + throw new Error("Module " + moduleName + " not supported"); + } + return new Contract(moduleAddress, CONTRACT_ABIS[moduleName], provider); +}; + +export const getModuleFactoryAndMasterCopy = ( + moduleName: KnownContracts, + provider: ethers.providers.JsonRpcProvider, + chainId: number +): { + moduleFactory: ethers.Contract; + moduleMastercopy: ethers.Contract; +} => { + const chainContracts = CONTRACT_ADDRESSES[chainId as SUPPORTED_NETWORKS]; + const masterCopyAddress = chainContracts[moduleName]; + const factoryAddress = chainContracts.factory; + const moduleMastercopy = getModuleInstance( + moduleName, + masterCopyAddress, + provider + ); + const moduleFactory = new Contract( + factoryAddress, + CONTRACT_ABIS.factory, + provider + ); + + return { + moduleFactory, + moduleMastercopy, + }; +}; diff --git a/src/factory/singleton-deployment.ts b/src/factory/singleton_factory.ts similarity index 52% rename from src/factory/singleton-deployment.ts rename to src/factory/singleton_factory.ts index ad7c733d..e3477cb2 100644 --- a/src/factory/singleton-deployment.ts +++ b/src/factory/singleton_factory.ts @@ -1,17 +1,21 @@ -import "hardhat-deploy"; -import "@nomiclabs/hardhat-ethers"; -import { task } from "hardhat/config"; +import { Contract } from "ethers"; import { HardhatRuntimeEnvironment } from "hardhat/types"; - const singletonFactoryAbi = [ "function deploy(bytes memory _initCode, bytes32 _salt) public returns (address payable createdContract)", ]; const singletonFactoryAddress = "0xce0042b868300000d44a59004da54a005ffdcf9f"; -const factorySalt = - "0xb0519c4c4b7945db302f69180b86f1a668153a476802c1c445fcb691ef23ef16"; -const AddressZero = "0x0000000000000000000000000000000000000000"; -const deployFactory = async (_: null, hardhat: HardhatRuntimeEnvironment) => { +/** + * Get the singleton factory contract (ERC-2470). + * If it is not deployed on the newtwork, then also deploy it. + * + * https://eips.ethereum.org/EIPS/eip-2470 + * @param hardhat + * @returns Singleton Factory contract + */ +export const getSingletonFactory = async ( + hardhat: HardhatRuntimeEnvironment +): Promise => { const [deployer] = await hardhat.ethers.getSigners(); console.log("Deployer address: ", deployer.address); @@ -42,63 +46,10 @@ const deployFactory = async (_: null, hardhat: HardhatRuntimeEnvironment) => { if ( (await hardhat.ethers.provider.getCode(singletonFactory.address)) == "0x" ) { - console.log( + throw Error( "Singleton factory could not be deployed to correct address, deployment haulted." ); - return; } } - console.log("Singleton Factory: ", singletonFactory.address); - - const Factory = await hardhat.ethers.getContractFactory("ModuleProxyFactory"); - // const singletonFactory = new hardhat.ethers.Contract(singletonFactoryAddress, singletonFactoryAbi) - - const targetAddress = await singletonFactory.callStatic.deploy( - Factory.bytecode, - factorySalt - ); - if (targetAddress == AddressZero) { - console.log( - "ModuleProxyFactory already deployed to target address on this network." - ); - return; - } else { - console.log("Target Factory Address:", targetAddress); - } - - const transactionResponse = await singletonFactory.deploy( - Factory.bytecode, - factorySalt - ); - - const result = await transactionResponse.wait(); - console.log("Deploy transaction: ", result.transactionHash); - - const factory = await hardhat.ethers.getContractAt( - "ModuleProxyFactory", - targetAddress - ); - - const factoryArtifact = await hardhat.artifacts.readArtifact( - "ModuleProxyFactory" - ); - - if ( - (await hardhat.ethers.provider.getCode(factory.address)) != - factoryArtifact.deployedBytecode - ) { - console.log("Deployment unsuccessful: deployed bytecode does not match."); - return; - } else { - console.log( - "Successfully deployed ModuleProxyFactory to target address! 🎉" - ); - } + return singletonFactory; }; - -task( - "singleton-deployment", - "Deploy factory through singleton factory" -).setAction(deployFactory); - -module.exports = {}; diff --git a/src/factory/types.ts b/src/factory/types.ts index df401fac..f3bbd416 100644 --- a/src/factory/types.ts +++ b/src/factory/types.ts @@ -13,6 +13,7 @@ export enum KnownContracts { SCOPE_GUARD = "scopeGuard", FACTORY = "factory", ROLES = "roles", + OZ_GOVERNOR = "ozGovernor", } type META_GUARD_VERSION = "v1.0.0"; diff --git a/src/tasks/singleton-deployment.ts b/src/tasks/singleton-deployment.ts new file mode 100644 index 00000000..ec49b392 --- /dev/null +++ b/src/tasks/singleton-deployment.ts @@ -0,0 +1,11 @@ +import { task } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { deployModuleFactory } from "../factory/deploy_module_factory"; + +export const deploy = async (_: null, hre: HardhatRuntimeEnvironment) => + deployModuleFactory(hre); + +task( + "singleton-deployment", + "Deploy factory through singleton factory" +).setAction(deploy);