Smart Contract Bytecode Verification

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

2 Likes