Smart Contract Bytecode Verification
This guide walks you through how to use OpenZeppelin Defender to verify a deployed smart contract against the bytecode of its build artifact.
Two options are presented in a sequence. First, you'll use Defender Admin client to deploy the contract and verify the bytecode. The second option illustrates how to automate bytecode verification using the Defender Hardhat plugin and Github Actions.
Setup
Initialize a new Github repository and clone locally, then create an empty Hardhat project:
$ cd [REPO_NAME]
$ yarn init -y
$ yarn install @nomiclabs/hardhat-ethers @openzeppelin/contracts
@openzeppelin/contracts-upgradeable @openzeppelin/hardhat-defender
@openzeppelin/hardhat-upgrades dotenv ethers hardhat
$ yarn hardhat
Next, remove the artifacts
folder from your .gitignore
so that the build artifacts will be pushed to the remote repository along with the contract code.
Create a .env
file:
$ touch .env
Add a Goerli private key, Defender team API key, and Infura/Alchemy API key to the file and save.
Update your hardhat.config.js
file to include the necessary API keys and import the required packages:
require('dotenv').config();
require('@nomiclabs/hardhat-ethers');
require('@openzeppelin/hardhat-upgrades');
require('@openzeppelin/hardhat-defender');
/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.9",
defender: {
apiKey: process.env.DEFENDER_API_KEY,
apiSecret: process.env.DEFENDER_API_SECRET,
},
networks: {
goerli: {
url: `https://goerli.infura.io/v3/${process.env.INFURA_PROJECT_ID}`,
accounts: [process.env.PRIVATE_KEY]
},
}
};
Write and Compile Contract
Go to the OpenZeppelin Contracts Wizard to quickly generate code for a basic upgradeable smart contract. Select Download --> Single File and save to /contracts
.
Compile the contract code to generate build artifacts and ABI:
$ yarn hardhat compile
Deploy and Verify Bytecode
Add the new files, commit and push to remote repository:
$ git add . && git commit -m "add build artifacts" && git push origin main
From Github, copy the build artifact URL: (e.g., https://raw.githubusercontent.com/[username]/[repo-name]/main/artifacts/build-info/1ca3e45b97b92b131128688d9d9faf68.json
)
You'll add it to the deploy script:
$ touch scripts/deploy.js
Open this file in a code editor and write a script that deploys the contract, adds the contract to the Admin dashboard, and submits the contract and build artifacts to Defender for verification:
const { ethers , upgrades, defender} = require('hardhat');
const {AdminClient} = require('defender-admin-client');
const {appendFileSync, readFileSync} = require('fs');
const NETWORK = 'goerli';
const NAME = "[YOUR_CONTRACT_NAME]";
const REPO_URL = `https://raw.githubusercontent.com/[YOUR_USERNAME]/[YOUR_REPO_NAME]/main/artifacts/build-info/7982d36063befc1a373b49d60bc13432.json`;
const contractABI = JSON.stringify(JSON.parse(readFileSync(`artifacts/contracts/${NAME}.sol/${NAME}.json`, 'utf8')).abi);
async function main() {
const adminClient = new AdminClient({apiKey: process.env.DEFENDER_API_KEY, apiSecret: process.env.DEFENDER_API_SECRET});
// deploy contract
const Contract = await ethers.getContractFactory(NAME);
const contract = await upgrades.deployProxy(Contract, { kind: 'uups' }).then(f => f.deployed());
console.log(`Deployed to: ${contract.address}\n`);
// add deployed contract to admin
const contractDetails = {
network: NETWORK,
address: contract.address,
name: NAME,
abi: contractABI,
};
const newAdminContract = await adminClient.addContract(contractDetails);
appendFileSync('.env', `\nADDRESS=${contract.address}`);
// verify compilation of deployed contract
const verification = await adminClient.verifyDeployment({
artifactUri: `${REPO_URL}`,
solidityFilePath: `contracts/${NAME}.sol`,
contractName: `${NAME}`,
contractAddress: `${contract.address}`,
contractNetwork: `${NETWORK}`,
});
console.log('Verification result: ', verification.matchType);
console.log('Compilation artifact: ', verification.artifactUri);
console.log('Network: ', verification.contractNetwork);
console.log('Contract address: ', verification.contractAddress);
console.log('SHA256 of bytecode on chain: ', verification.onChainSha256);
console.log('SHA256 of provided compilation artifact: ', verification.providedSha256);
console.log('Compilation artifact provided by: ', verification.providedBy);
console.log('Last verified: ', verification.lastVerifiedAt);
}
main().catch(console.error);
Run the script:
$ yarn hardhat run scripts/deploy.js --network goerli
Congrats! Now the contract code is deployed, loaded into the Defender Admin dashboard, and the bytecode of the build artifacts have been verified.
Automate Upgrade Proposal and Verification (using Github Actions)
To integrate bytecode verification into your existing deployment pipeline, add the following .yml
file to your Github repository in /.github/workflows, supplying the appropriate contract address and owner:
---
name: CI
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-20.04
timeout-minutes: 10
env:
# Hardhat connection settings
INFURA_PROJECT_ID: "${{ secrets.INFURA_PROJECT_ID }}"
PRIVATE_KEY: "${{ secrets.PRIVATE_KEY }}"
steps:
- uses: actions/checkout@v2
- name: Use node@14
uses: actions/setup-node@v1
with: {node-version: 14.x}
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Node modules cache
uses: actions/cache@v2
id: yarn-cache
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
~/.cache/node-gyp-cache
key: ${{ runner.os }}-yarn-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock')
}}
restore-keys: |
${{ runner.os }}-yarn-${{ env.cache-name }}-
env:
cache-name: v1
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Compile contracts
run: yarn build
- name: Save build artifacts
uses: actions/upload-artifact@v2
with:
name: artifacts
path: artifacts
propose-upgrade:
name: Upgrade
runs-on: ubuntu-20.04
timeout-minutes: 10
needs: build
env:
# Hardhat connection settings
INFURA_PROJECT_ID: "${{ secrets.INFURA_PROJECT_ID }}"
PRIVATE_KEY: "${{ secrets.PRIVATE_KEY }}"
steps:
- uses: actions/checkout@v2
- name: Use node@14
uses: actions/setup-node@v1
with: {node-version: 14.x}
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- name: Node modules cache
uses: actions/cache@v2
id: yarn-cache
with:
path: |
${{ steps.yarn-cache-dir-path.outputs.dir }}
~/.cache/node-gyp-cache
key: ${{ runner.os }}-yarn-${{ env.cache-name }}-${{ hashFiles('**/yarn.lock')
}}
restore-keys: |
${{ runner.os }}-yarn-${{ env.cache-name }}-
env:
cache-name: v1
- name: Install dependencies
run: yarn --frozen-lockfile
- name: Get build artifacts
uses: actions/download-artifact@v2
with:
name: artifacts
path: artifacts
- name: Show downloaded build artifacts
run: ls -R artifacts
- name: Propose upgrade to new version
run: yarn hardhat run scripts/propose-and-verify.js --network goerli
env:
# URL used to reference the bytecode verification
WORKFLOW_URL: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
# Address of the contract to upgrade
ADDRESS: "0xe70888b0eeefC901bB497D7649c63339da858615"
# Upgradeability owner
OWNER: "0x6084fBE2Aa96Bb131D6Bc7Bd3BE786882cfA250F"
# API keys to Defender
DEFENDER_API_KEY: "${{ secrets.DEFENDER_API_KEY }}"
DEFENDER_API_SECRET: "${{ secrets.DEFENDER_API_SECRET }}"
This will execute the Github Action triggered by each push to the repository.
As a next step, in Github, head over to Settings --> Secrets --> Actions --> New repository secret.
Supply the necessary environment variables.
Finally, you will need to write the script that creates a new proposal and submits the bytecode for verification.
$ touch scripts/propose-and-verify.js
Add the following code, ensuring the appropriate variables are supplied:
const { defender } = require('hardhat');
// deploy v2 implementation and propose upgrade, submitting bytecode for verification
const owner = process.env.OWNER
const address= process.env.ADDRESS
const url = process.env.WORKFLOW_URL
async function main() {
const proposal = await defender.proposeUpgrade(address, 'VotingToken',{
bytecodeVerificationReferenceUrl: url,
kind: 'uups',
description: `Upgrading to new version deployed at ${url}`,
multisig: owner,
multisigType: 'EOA',
});
// multisig options: 'Gnosis Safe' | 'Gnosis Multisig' | 'EOA';
// Gnosis Safe is the latest offering. it is what's created if you use Defender to create your multisig.
// Gnosis multisig - a legacy offering of gnosis prior to the Safe
// EOA - eg metamask, a single signer account
const verification = proposal.verificationResponse;
console.log(`Created new upgrade proposal at ${proposal.url} for artifact with digest ${verification?.providedSha256 ?? 'unknown'} (match ${verification.matchType})`);
}
main().catch(console.error);
Well done! Head over to defender.openzeppelin.com and you will see that there is an upgrade proposal awaiting you on the Admin dashboard.
Reference