Based in the concept repo by Santiago Palladino
Implementation by Raúl Martínez
This is smart contract deployment and admin transaction pipeline using Github Actions. The pipeline is composed a set of build, test, deployment, verification, and post-deploy configurable Hardhat tasks and scripts.
In case there are problems with Github, the tasks and config files developed can be triggered locally as well.
This pipeline also serves to propose a list of batched multisig transactions for the Forta Council to sign.
Unlike traditional software deployments, contract deployments are usually run by a developer from their local workstation. This is not just a hassle for the developer, but it also hinders transparency and reproducibility of deployments. By moving them to a publicly auditable deployment pipeline, anyone can follow the trace from the source code to a deployed address.
This is especially valuable for protocol stakeholders (from signers to community members) who need to review the deployment or upgrade to a specific version of the code. A stakeholder needs to know if the code deployed matches a specific version, often described by a git tag or commit, that has already been reviewed or even audited. Having a public compilation and deployment pipeline makes it easy to check that the bytecode deployed at a specific address was generated out of a given version of the source code.
For multisig actions in small teams, there are also several risks and errors, like using wrong input data or other human errors when handling the scripts and the poor legibility of batched transactions through Multicall.
The release workflow is triggered by pull requests from release/
branches, and runs on the release branch, not on the merge commit. Each stage in the workflow is set up as a sequential job, which can be mapped to common hardhat tasks or custom scripts.
The deployment workflow autogenerates a summary with deployment info and relevant links. It will also alert if there is a diff in the contracts between the audited commit and the deployed one, when you supply the optional parameter audited
An initial build job compiles contracts and saves them as artifacts for the following jobs. No other job recompiles contracts, to ensure the same bytecode is preserved throughout the workflow.
Unit tests are run on the contracts to be released, using hardhat test
. Other contract test runners could be used as well.
A preparation step parses release info from the user-defined yaml, and outputs an initial release summary. Any diffs with the audited commit are shown here, so the deployment can be halted if needed.
This tasks uses the defined secret <NETWORK_NAME>_MNEMONIC
or <NETWORK_NAME>_PRIVATE_KEY
to deploy contracts. Rationale for using a low-security private key is that the deployer account should not have any privileges in the system, and the only risk of leaking it is losing the funds for paying deployment fees. Still, a managed solution such as a Defender Relay could be used instead.
The deployment script should output the deployed contracts to a deployed.json
file or prepared-upgrades.json
in the release folder (under output subfolder), and update a global release/deployments/<network_id>.json
registry. The provided deploy
task provides deployed.json
, prepare-upgrade
provides prepared-upgrades.json
, while deploy-and-prepare-upgrade
does both. Once finished, it commits those changes into the release branch, so once merged, the latest deployment info is kept on the main branch.
The verification step is meant to verify source code and artifacts. The provided verify-deployed
task will read newly deployed contracts out of the deployed.json
file, and verify source on Etherscan (or XXXScan family of EVM block explorers) and bytecode on Defender. Note that this job can fail, yet the post-deploy step runs anyway. Verification is separate from deployment so it can be re-run if needed.
The post-deploy job is intended to cover any actions that are meant to be run after the deployment.
The main use case for this step is the propose-admin
task, which creates a new batch multisig proposal to upgrade all contracts for which a new implementation was deployed reading prepared-upgrades.json
, and also batch together multisig transaction interacting with contracts of the repo by parsing propose-admin.json
config files.
The upgrade proposal is created in OpenZeppelin Defender:
To trigger a new release, create a folder with the version identifier X.Y(.Z)
in the releases
folder, with an index.yml
file with the spec of the release. This includes:
title
: Title of the releasedescription
: Description of what this release is about.network
: The network where to deploy, needs to be defined in the hardhat config fileaudited
: Optional commit in which the code was audited, used to show the diff with the deployed versiondeploy
: Deployment command to executeverify
: Verification command to executefinish
: Wrap-up command to execute
All commands are passed to yarn hardhat
, so any hardhat task can be used, although we recommend to use the predefined tasks explained next.
Example:
title: Stake Delegation
network: mumbai
deploy: deploy-and-prepare-upgrade 1.1.0
verify: verify-deployed
finish: propose-admin
description: |
## Deployed contracts:
- RewardsDistributor
- StakeAllocator
- ScannerPoolRegistry
- ScannerToScannerPoolMigration
## Deployed new implementations:
- FortaStaking -> 0.1.2
- FortaStakingParameters --> StakeSubjectGateway 0.1.1
- ScannerRegistry -> 0.1.4
- AgentRegistry -> 0.1.6
- Dispatch -> 0.1.5
## Output tx:
- Upgrade implementations
- Grant required roles
- Set managed stake thresholds for pools
- Wires together staking contracts
- Configures migration in ScannerRegistry (sunsetting time included)
You also need to create a subfolder with the network name, where the config json files that the tasks consume will be located, and the output files will be written.
For simplicity, some tasks are already defined:
deploy-proxy CONTRACT ARG1 ARG2...
: Deploys a contract as upgradeable and initializes it with ARGsprepare-upgrade CONTRACT1 CONTRACT2
: Deploys new implementations for all CONTRACTsverify-deployed
: Reads deployed contracts and verifies source code in Etherscan and artifacts in Defenderpropose-upgrade
: Creates a batch proposal to simultaneously upgrade all contracts via a multisig in Defender
Alternatively, custom scripts can be used by passing them through hardhat's run
command. This allows for more complex deployment scripts to be run. See v1.4/deploy.ts for an example.
Once the release is defined, create a pull request from the release/X.Y(.Z)
branch into master. This will trigger the release workflow, that includes the build, test, deployment, verification, and post-deploy jobs.
- <release_version>: use `X.Y(.Z)` for a deployment, a short description for an admin action only.
- <network_name>: network name, i.e polygon, mainnet, mumbai. It has to be the ones defined in hardhat.config.js
- config: folder for the json inputs, if needed.
- deploy.json: Input for the deploy task with info for contracts to be deployed.
- upgrade.json: Input for the prepare-upgrade task, with implementation contracts info.
- propose-admin: Input for the propose-admin task, with the methods that the multisig needs to execute in the repo's contract
- output: folder generated by the tasks
- deployed: info of the new contracts or proxy + implementation contracts deployed.
- prepared-upgrades: implementation contracts deployed by oz-upgrades plugin
- deployments: folder with general info of the contracts deployed in several networks.
- <network_id>.json : info on the current state of the contracts deployed on the network
- multisigs.json: info on the admin multisigs on each network
- relayers.json: info on the (usually) priviledged EOAs needed. Tipically OZ Defender relayers.
These json files hold the info that the tasks will use to interact with the contracts.
To make the config files less error prone, there are some parameter substitutions allowed:
deployment.<contract-key>
: address of a deployed contract/proxy found in/releases/deployments/<network>.json
. Exampledeployment.access-manager
. This could reference contracts being deployed in the same release, as long as they precede this reference in the config file (previous entries).deployment.multisig
: admin multisig address for the network as defined in/releases/deployments/multisigs.json
.deployment.relayer
: priviledged EOAs (usually OZ Defender Relayers) for the network as defined in/releases/deployments/relayers.json
.roles.<IDENTIFIER_ROLE>
: keccack256 hash of the identifier of a role, as defined inRoles.sol
or the Forta token contracts.- NOTE: All parameters must be strings, numbers included, to not trigger overflow errors on BigNumber conversion.
For deploying and preparing upgrades, they will describe contracts and their deployment parameters.
- Non-upgradeable contract
"ContractName": { --> Contract name
"constructor-args": [
"0x123...00", "argument", "1" --> arguments for contructor.
]
}
- Upgradeable contract
"ContractName": { --> Contract name
"impl": { --> Implementation params
"init-args": [], --> Init arguments.
"opts": { --> Arguments for oz-upgrades opts field
}
}
},
See https://docs.openzeppelin.com/upgrades-plugins/1.x/api-hardhat-upgrades#common-options
Example:
"Dispatch": {
"impl": {
"opts": {
"unsafe-allow": [
"delegatecall"
],
"constructor-args": [
"deployment.forwarder"
]
}
}
}
This task will parse this file and add batched steps to the upgrade multisig transaction. This could be used in a release without deploy steps to just batch multisig actions in an auditable process.
The workflow requires a few secrets to be set in order to run, such as:
- INFURA_PROJECT_ID
- MUMBAI_MNEMONIC
- MUMBAI_NODE
- POLYGON_MNEMONIC
- POLYGON_NODE
- POLYSCAN
- RELEASE_PATH
- ARTIFACT_REFERENCE_URL
The release CI/CD related scripts are under the MIT License. Forta Network contracts are under the Forta Network License.