Skip to content

Commit

Permalink
Merge pull request #6 from gnosis/update-deployment-scripts
Browse files Browse the repository at this point in the history
Improved deployment scripts
  • Loading branch information
asgeir-s authored Jan 5, 2023
2 parents 901b479 + b7b42cc commit 649adef
Show file tree
Hide file tree
Showing 12 changed files with 1,210 additions and 950 deletions.
22 changes: 12 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,24 +25,26 @@ yarn deploy # deploys the contracts add the `--network` param to select a networ

## Deployment

This project is set up to support both a "normal deployment" where the module is deployed directly, along with deployment via the Mastercopy / Minimal Proxy pattern (using our ModuleProxyFactory).

Currently, it is set up to deploy via the Mastercopy / Minimal Proxy pattern on Goerli and as a "normal deployment" on other networks. You can easily modify this behavior for your own module.
This project is set up for deployment via the Mastercopy / Minimal Proxy pattern (using our ModuleProxyFactory).

```
yarn deploy # "normal deployment"
yarn deploy --network goerli # deploys a mastercopy and a minimal proxy for the module
yarn deploy
```

The "normal deployment" can be useful for easily deploying and testing your module locally (for instance, the Hardhat Network).

The "normal deployment" deploys the MyModule contract and the test contracts (`contracts/test/Button.sol` and `contracts/test/TestAvatar.sol`), then sets the TestAvatar as the Button owner, and enables MyModule on the TestAvatar.
This will set up a minimal sample Zodiac Module in the following steps:

The Mastercopy / Minimal Proxy deployment deploys the MyModule mastercopy, a MyModule proxy, and the test contracts (contracts/test/Button.sol and contracts/test/TestAvatar.sol), then sets the TestAvatar as the Button owner and enables the MyModule proxy on the TestAvatar.
1. Deploy the Singleton Factory [EIP-2470](https://eips.ethereum.org/EIPS/eip-2470), if its not already deployed to the current chain.
This is used for deploying Zodiac Modules (and the Module Factory) to the same address deterministic across chains.
2. Deploy the Module, MyModule (`contracts/MyModule.sol`).
3. Deploy the Zodiac Module Factory.
4. Deploy a Test Avatar.
5. Deploy a Button contract.
6. Set the Test Avatar as the Button owner.
7. Deploy a minimal proxy (clone) of the MyModule Module using the Zodiac Module Factory.

### Mastercopy and minimal proxys

When deploying modules that are going to be used for multiple avatars, it can make sense to use our Mastercopy/Proxy pattern. This deployment uses the Singleton Factory contract (EIP-2470). See a list of supported networks [here](https://blockscan.com/address/0xce0042B868300000d44A59004Da54A005ffdcf9f). For adding support to other chains, check out the documentation [here](https://github.com/gnosis/zodiac/tree/master/src/factory#deployments) and [here](https://eips.ethereum.org/EIPS/eip-2470).
This deployment uses the Singleton Factory contract (EIP-2470). See a list of supported networks [here](https://blockscan.com/address/0xce0042B868300000d44A59004Da54A005ffdcf9f). For adding support to other chains, check out the documentation [here](https://github.com/gnosis/zodiac/tree/master/src/factory#deployments) and [here](https://eips.ethereum.org/EIPS/eip-2470).

## Attache your module to a Gnosis Safe

Expand Down
45 changes: 45 additions & 0 deletions deploy/01_mastercopy_module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ethers } from "hardhat"
import "hardhat-deploy"
import { DeployFunction } from "hardhat-deploy/types"
import { HardhatRuntimeEnvironment } from "hardhat/types"
import { computeTargetAddress, deployMastercopy } from "@gnosis.pm/zodiac"
import MODULE_CONTRACT_ARTIFACT from "../artifacts/contracts/MyModule.sol/MyModule.json"

const FirstAddress = "0x0000000000000000000000000000000000000001"
const Salt = "0x0000000000000000000000000000000000000000000000000000000000000000"

const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
const contract = await ethers.getContractFactory("MyModule")

let address = await deployMastercopy(
hre,
contract,
[
FirstAddress, // owner
FirstAddress, // button
],
Salt,
)

if (address === ethers.constants.AddressZero) {
// the mastercopy was already deployed
const target = await computeTargetAddress(
hre,
contract,
[
FirstAddress, // owner
FirstAddress, // button
],
Salt,
)
address = target.address
}

hre.deployments.save("MyModuleMastercopy", {
abi: MODULE_CONTRACT_ARTIFACT.abi,
address,
})
}

deploy.tags = ["moduleMastercopy"]
export default deploy
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
import "hardhat-deploy"
import { DeployFunction } from "hardhat-deploy/types"
import { HardhatRuntimeEnvironment } from "hardhat/types"
import { deployModuleFactory } from "@gnosis.pm/zodiac/dist/src/factory/deployModuleFactory"

const deploy: DeployFunction = async function ({ deployments, getNamedAccounts, ethers }: HardhatRuntimeEnvironment) {
console.log("Deploying 'external' dependencies")
const deploy: DeployFunction = async function (hre: HardhatRuntimeEnvironment) {
console.log("Deploying 'external' dependencies (Button and Avatar)")
const { deployments, getNamedAccounts, ethers } = hre
const { deploy } = deployments
const { deployer } = await getNamedAccounts()

// Deploys the ModuleFactory (and the Singleton factory) if it is not already deployed
await deployModuleFactory(hre)

const testAvatarDeployment = await deploy("TestAvatar", {
from: deployer,
})
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import "hardhat-deploy"
import { DeployFunction } from "hardhat-deploy/types"
import { HardhatRuntimeEnvironment } from "hardhat/types"
import { deployAndSetUpCustomModule } from "@gnosis.pm/zodiac/dist/src/factory/factory"
import { deployAndSetUpCustomModule, ContractAddresses, KnownContracts, SupportedNetworks } from "@gnosis.pm/zodiac"
import MODULE_CONTRACT_ARTIFACT from "../artifacts/contracts/MyModule.sol/MyModule.json"

const deploy: DeployFunction = async function ({
deployments,
Expand All @@ -16,9 +17,18 @@ const deploy: DeployFunction = async function ({
const buttonDeployment = await deployments.get("Button")
const testAvatarDeployment = await deployments.get("TestAvatar")

const myModuleMastercopyDeployment = await deployments.get("MyModule")
const myModuleMastercopyDeployment = await deployments.get("MyModuleMastercopy")

const chainId = await getChainId()
const network: SupportedNetworks = Number(chainId)

if ((await ethers.provider.getCode(ContractAddresses[network][KnownContracts.FACTORY])) === "0x") {
// the Module Factory should already be deployed to all supported chains
// if you are deploying to a chain where its not deployed yet (most likely locale test chains), run deployModuleFactory from the zodiac package
throw Error("The Module Factory is not deployed on this network. Please deploy it first.")
}

console.log("buttonDeployment.address:", buttonDeployment.address)

const { transaction } = deployAndSetUpCustomModule(
myModuleMastercopyDeployment.address,
Expand All @@ -33,10 +43,14 @@ const deploy: DeployFunction = async function ({
)
const deploymentTransaction = await deployerSigner.sendTransaction(transaction)
const receipt = await deploymentTransaction.wait()
console.log(receipt)
const myModuleProxyAddress = receipt.logs[1].address
console.log("MyModule minimal proxy deployed to:", myModuleProxyAddress)

deployments.save("MyModuleProxy", {
abi: MODULE_CONTRACT_ARTIFACT.abi,
address: myModuleProxyAddress,
})

// Enable MyModule as a module on the safe to give it access to the safe's execTransactionFromModule() function
const testAvatarContract = await ethers.getContractAt("TestAvatar", testAvatarDeployment.address, deployerSigner)
const currentActiveModule = await testAvatarContract.module()
Expand Down
22 changes: 0 additions & 22 deletions deploy/mastercopy-proxy/01_mastercopy_module.ts

This file was deleted.

34 changes: 0 additions & 34 deletions deploy/raw/01_dependencies.ts

This file was deleted.

35 changes: 0 additions & 35 deletions deploy/raw/02_module.ts

This file was deleted.

4 changes: 0 additions & 4 deletions hardhat.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ const config: HardhatUserConfig = {
goerli: {
url: process.env.GOERLI_URL || "",
accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [],
deploy: ["deploy/mastercopy-proxy"], // deploy via mastercopy and a proxy
},
},
namedAccounts: {
Expand All @@ -31,9 +30,6 @@ const config: HardhatUserConfig = {
etherscan: {
apiKey: process.env.ETHERSCAN_API_KEY,
},
paths: {
deploy: "deploy/raw", // normal deployment
},
}

export default config
33 changes: 17 additions & 16 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@
},
"homepage": "https://github.com/gnosis/zodiac-mod-starter-kit",
"devDependencies": {
"@gnosis.pm/zodiac": "1.0.8",
"@nomiclabs/hardhat-ethers": "^2.0.0",
"@nomiclabs/hardhat-etherscan": "^3.0.0",
"@nomiclabs/hardhat-waffle": "^2.0.0",
"@typechain/ethers-v5": "^10.0.0",
"@typechain/hardhat": "^6.0.0",
"@gnosis.pm/zodiac": "2.1.2",
"@nomiclabs/hardhat-ethers": "^2.2.1",
"@nomiclabs/hardhat-etherscan": "^3.1.3",
"@nomiclabs/hardhat-waffle": "^2.0.3",
"@typechain/ethers-v5": "^10.2.0",
"@typechain/hardhat": "^6.1.5",
"@types/chai": "^4.2.21",
"@types/mocha": "^9.1.0",
"@types/node": "^12.0.0",
"@types/node": "^16.0.0",
"@typescript-eslint/eslint-plugin": "^5.12.0",
"@typescript-eslint/parser": "^5.16.0",
"chai": "^4.3.6",
Expand All @@ -45,19 +45,20 @@
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-promise": "^6.0.0",
"ethereum-waffle": "^3.4.0",
"ethers": "^5.6.2",
"hardhat": "^2.9.2",
"hardhat-deploy": "^0.11.4",
"hardhat-gas-reporter": "^1.0.8",
"ethers": "^5.7.1",
"hardhat": "^2.12.4",
"hardhat-deploy": "^0.11.22",
"hardhat-gas-reporter": "^1.0.9",
"prettier": "^2.6.1",
"prettier-plugin-solidity": "^1.0.0-beta.19",
"solhint": "^3.3.7",
"solidity-coverage": "^0.7.20",
"ts-node": "^10.7.0",
"typechain": "^8.0.0",
"typescript": "^4.6.3"
"solidity-coverage": "^0.8.2",
"ts-node": "^10.9.1",
"typechain": "^8.1.1",
"typescript": "^4.9.4"
},
"dependencies": {
"@openzeppelin/contracts": "^4.5.0"
"@gnosis.pm/safe-contracts": "^1.3.0",
"@openzeppelin/contracts": "^4.8.0"
}
}
14 changes: 7 additions & 7 deletions test/myModule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@ import { expect } from "chai"
import { ethers, deployments, getNamedAccounts } from "hardhat"

const setup = async () => {
await deployments.fixture(["MyModule"])
await deployments.fixture(["moduleProxy"])
const { tester } = await getNamedAccounts()
const testSigner = await ethers.getSigner(tester)
const buttonDeployment = await deployments.get("Button")
const myModuleDeployment = await deployments.get("MyModule")
const myModuleProxyDeployment = await deployments.get("MyModuleProxy")
const buttonContract = await ethers.getContractAt("Button", buttonDeployment.address, testSigner)
const myModuleContract = await ethers.getContractAt("MyModule", myModuleDeployment.address, testSigner)
return { buttonContract, myModuleContract }
const myModuleProxyContract = await ethers.getContractAt("MyModule", myModuleProxyDeployment.address, testSigner)
return { buttonContract, myModuleProxyContract }
}

describe("MyModule", function () {
it("Should be possible to 'press the button' through MyModule", async function () {
const { buttonContract, myModuleContract } = await setup()
const { buttonContract, myModuleProxyContract } = await setup()
expect(await buttonContract.pushes()).to.equal(0)
await myModuleContract.pushButton()
await myModuleProxyContract.pushButton()
expect(await buttonContract.pushes()).to.equal(1)
})
it("Should NOT be possible to 'press the button' directly on the Button contract", async function () {
const { buttonContract } = await setup()
expect(buttonContract.pushButton()).to.revertedWith("Ownable: caller is not the owner")
await expect(buttonContract.pushButton()).to.revertedWith("Ownable: caller is not the owner")
})
})
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"resolveJsonModule": true,
"strict": true,
"esModuleInterop": true,
"outDir": "dist",
Expand Down
Loading

0 comments on commit 649adef

Please sign in to comment.