Deploy Upgradable Smart Contract Securely with Defender 2.0 by OpenZeppelin

Deploy Upgradable Smart Contract Securely with Defender 2.0 by OpenZeppelin

Hey guys! In this guide, we see how we can deploy and upgrade smart contracts securely with OpenZeppelin Defender Deploy Module together with Gnosis Safe, uncovering its features and advantages.

We will also have a step-by-step tutorial to help you get started with upgradable smart contracts.

  1. Check the Github Repository with the tutorial.
  2. Get early access to Defender 2.0
  3. Watch the complete video tutorial:

Defender 2.0

OpenZeppelin Defender is an all-in-one security platform for decentralised application developers. It offers a suite of tools and services designed to protect smart contracts throughout their lifecycle. Among these tools, the Deploy Module plays an important role in ensuring the secure deployment of smart contracts.

Defender 2.0 Deploy Module

The OpenZeppelin Defender Deploy?Module

The Deploy Module is designed to streamline and secure the process of deploying smart contracts. Let’s explore its core features and functionalities:

Bytecode Verification

When you compile a smart contract and deploy it, Defender hashes the local bytecode on your development machine and compares it to the bytecode deployed on the blockchain. This verification process guarantees that the contract deployed on the blockchain is precisely the same as the one developed locally. This verification feature provides several key advantages:

  • Preventing Man-in-the-Middle Attacks: Bytecode verification prevents potential attacks by ensuring that no one can modify your contract’s bytecode during deployment.?
  • Defeating Malicious RPCs: Some malicious RPC (Remote Procedure Call) providers might tamper with your contract’s bytecode before deployment. Bytecode verification helps maintain the integrity of your code, protecting it from unwanted modifications.
  • Comparing Audited and Onchain Code: After a smart contract audit, you can lock the commit hash of the audited code and compare it to the on-chain deployed contract. This ensures that the contract on the blockchain matches the audited code.

Bytecode Verification Feature

Integration with MultiSig (Gnosis?Safe)

Integration with Gnosis Safe

Gnosis Safe is a standard for managing smart contracts by DeFi protocol teams. Defender seamlessly integrates with Safe, making it incredibly easy to use Gnosis Safe for the approval process. Here’s how this integration works:

  • Streamlined Deployment: Defender becomes the central hub for your contract deployments, creating a smoother and more secure experience.
  • Multi-Sig Wallet Control: The ownership and control of the contract are initially transferred to a multi-signature wallet (Gnosis Safe) to enhance security. This multi-sig wallet becomes the gatekeeper for authorizing contract upgrades and changes.

Creating a New Safe (Multisig Wallet)

Integration with Blockchain Explorers

Defender takes the hassle out of verifying your smart contracts on blockchain explorers. By supplying API keys for Ethereum explorers like Etherscan, Arbitrum Scan, and various testnets, Defender automatically verifies your contracts.

Cost-Efficiency and Deterministic Deployment

One of the standout features of Defender is its cost-efficiency and deterministic deployment process. Here’s why this matters:

  • Free Deployments on Testnets: When deploying to test networks like Goerli or Sepolia, you don’t have to chase down test ETH from faucets. Defender automatically provides relayers funded with test ether to facilitate these deployments.
  • Deterministic Contract Deployment: When deploying across different EVM blockchains, you can ensure that the contracts have the same address on each chain. This is a vital feature to prevent cross-chain replay attacks and confusion over contract addresses.

Cost-Effective Deterministic Deployments

Defender 2.0 Deploy Module in Action: Demo 1- Deploying a non-upgradable ERC20?Token

Setting Up Our Test Environment

In the defender deploy module we will click the setup button to start setting up our test environment:

Setting up our test environment

Now select the network you want to deploy your contracts to, in my example, I selected Goerli and Sepolia:

Selecting Goerli And Sepolia

In order for Defender to automatically verify your smart contracts on blockchain explorers, insert the API keys of the blockchain explorer site (e.g Etherscan) in the next step:

Adding Blockchain Explorer API Keys

Choose a relayer for test environment deployments. If you don’t have one, Defender 2.0 will create it for you. Relayers handle gas fee payments, secure key storage, transaction signing, nonce management, gas price estimation, and resubmissions, streamlining the deployment process, so you want have to pay for your smart contract deployments:

Relayers Setup

Since we are going to deploy a non-upgradable smart contract in our first demo, we don’t have to setup an upgrade approval process yet, therefore, we are going to click “Skip this step” for now:

Upgrade Approval Process

Now, Defender 2.0 will generate an API Key and Secret for the newly created setup, copy, and store them safely. Click on Let’s Deploy:

Copying the API Key and Secret

The next time you click the “deploy” module you will see that your setup environment is ready:

Our Test Environment is Ready!

Creating a New Hardhat?Project

We will create a new hardhat project by executing the following command in our terminal, alternatively, you can clone the following repository which already consists of all the tutorial files.

mkdir secure-deployment && cd secure-deployment && npx hardhat        

We will choose typescript, and click enter for all the rest (default options). Now, install some dependencies by executing the following command in your terminal:

npm i @openzeppelin/hardhat-upgrades @openzeppelin/contracts-upgradeable dotenv --save-dev        

We will open the folder using VSCode and this is how the project structure should look like:

Hardhat Project Folder Structure

We will change the hardhat.config.ts file, and overwrite it with the following code:

import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import "@openzeppelin/hardhat-upgrades";

require("dotenv").config();

const config: HardhatUserConfig = {
  solidity: "0.8.19",
  defender: {
    apiKey: process.env.DEFENDER_KEY as string,
    apiSecret: process.env.DEFENDER_SECRET as string,
  },
  networks: {
    goerli: {
      url: "https://rpc.ankr.com/eth_goerli",
      chainId: 5
    },
  },
};

export default config;        

  1. We imported @openzeppelin/hardhat-upgrades library,
  2. We set the defender apiKey and apiSecret
  3. We added the Goerli RPC to the networks sections

In the root folder, we are going to create a?.env file and store there the Defender’s apiKey and apiSecret:

Our .env file

Creating a Non-Upgradable ERC20?token

We will use OpenZeppelin Contracts Wizard to create a new simple ERC20 token. Create a new file NonUpgradableToken.sol in the contracts/ folder, and paste the following code inside:

// SPDX-License-Identifier: MIT
pragma solidity 0.8.19;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract NonUpgradableToken is ERC20 {
    constructor() ERC20("NonUpgradableToken", "NUT") {}
}        

Writing our deployment script

Create a new file deployToken.ts inside the script/ folder, and place the following code inside:

import { ethers, defender } from "hardhat";

async function main() {
  const Token = await ethers.getContractFactory("NonUpgradableToken");

  const deployment = await defender.deployContract(Token);
  await deployment.waitForDeployment();
  console.log(`Contract deployed to: ${await deployment.getAddress()}`)
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});        

  1. We import defender from Hardhat.
  2. We use the defender.deployContract to deploy a non-upgradable contract.
  3. We wait for the deployment to end and print the newly deployed contract address.

Deploying the?Contract

Now we will execute the following command in our Terminal:

npx hardhat run --network goerli scripts/deployToken.ts        

After the execution is finished we can see the newly deploy smart contract address:

Deploying a non-upgradable contract

Also, if we go to the Defender dashboard we will see the new deployment as well:

New Deployed Contract on Defender Deploy Module

We can always click on it to get extra details:

New Deployed Contract Details

Defender 2.0 Deploy Module in Action: Demo 2 - Deploying an Upgradable Contract

Setting up?Safe

First, we will need to setup a Gnosis Safe Multisig wallet. To set up a Safe:

  1. Open the Safe app in your web browser.
  2. Connect your wallet to the Goerli testnet.
  3. Click “Create new Account” and follow the steps.

Creating a New Safe

In our example, I created a safe of 3 owners and the threshold number is 2 (2 out of 3 signatures are required to execute a transaction).

You can copy your safe address and store it aside:

Our New Safe Address

Creating an “Upgrade Approval?Process”

Remember the step that we skipped in the setup process? Now we will have to configure it so we can securely upgrade smart contracts using Safe. In Defender head over to “Manage” and then to “Approval Processes”, then click “Add new Approval Process”:

Adding a New Approval Process


Now complete the following information and click “Save Changes”:

New Approval Process

Head back to the Deploy page, and click “Configure” on the Goerli Network:

Configure the Goerli Network deployments

Now you can select your approval process and click “Save Changes”:

Linking an Approval Process to a Network

Writing our Upgradable Contract

In your hardhat project inside the contracts\ folder create a file Box.sol, and paste the following code:

// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.19;

import {Initializable} from  "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts-upgradeable/proxy/utils/UUPSUpgradeable.sol";
import {OwnableUpgradeable} from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol";
/// @title Box
/// @notice A box with objects inside.
contract Box is Initializable, UUPSUpgradeable, OwnableUpgradeable {
    /*//////////////////////////////////////////////////////////////
                                VARIABLES
    //////////////////////////////////////////////////////////////*/
    /// @notice Number of objects inside the box.
    uint256 public numberOfObjects;
    /*//////////////////////////////////////////////////////////////
                                FUNCTIONS
    //////////////////////////////////////////////////////////////*/
    /// @notice No constructor in upgradable contracts, so initialized with this function.
    function initialize(uint256 objects, address multisig) public initializer {
        __UUPSUpgradeable_init();
        __Ownable_init();
        numberOfObjects = objects;
        // Initialize OwnableUpgradeable explicitly with given multisig address.
        transferOwnership(multisig);
    }
    /// @notice Remove an object from the box.
    function removeOneObject() external {
        require(numberOfObjects > 1, "Nothing inside");
        numberOfObjects -= 1;
    }
    /// @dev Upgrades the implementation of the proxy to new address.
    function _authorizeUpgrade(address) internal override onlyOwner {}
}        

  1. This is a simple smart contract that represents a box.
  2. Upon initialization the contract transfers the ownership to the multisig wallet.

Creating our Deploy?Script

In the scripts\ folder we will create a new file deploy.ts and paste the following code:

import { ethers, defender } from "hardhat";

async function main() {
  const Box = await ethers.getContractFactory("Box");
  const defaultApprovalProcess = await defender.getDefaultApprovalProcess();
  if (defaultApprovalProcess.address === undefined) {
    throw new Error(`Upgrade approval process with id ${defaultApprovalProcess.approvalProcessId} has no assigned address`);
  }
  const deployment = await defender.deployProxy(Box, [5, defaultApprovalProcess.address], {
    initializer: "initialize",
    redeployImplementation: "always"
  });
  await deployment.waitForDeployment();
  console.log(`Contract deployed to ${await deployment.getAddress()}`);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});        

  1. This time we’re using the defender.deployProxy function
  2. We’re making sure we have an approval process and extracting the safe address from its object.
  3. We pass the safe address to the Box initialization function to make sure the ownership is being transferred correctly.

Deploying our Upgradable Box?Contract

Now we will execute the following command in our Terminal:

npx hardhat run --network goerli scripts/deploy.ts        

After the execution is finished we can see the newly deployed smart contract proxy:

Newly Deployed Upgradable Contract (Proxy Address)

Also, if we go to the Defender dashboard we will see two new smart contracts, the Box Implementation and en ERC1976 proxy:

Upgradable Contract Deployment

Congratulations, you deployed an upgradable smart contract using Defender. Now let’s see how we can upgrade it using Safe ??

Defender 2.0 Deploy Module in Action: Demo 3— Upgrading Our?Contract

Let’s see how we can upgrade our smart contract and point the proxy to a new implementation in a safe way using Defender and Safe.

Creating a New Implementation

In your hardhat project, create a new file BoxV2.sol inside the contracts/ folder and paste the following code:

// SPDX-License-Identifier: Unlicense
pragma solidity 0.8.19;

import {Box} from "./Box.sol";

/// @title BoxV2
/// @notice An improved box with objects inside.
contract BoxV2 is Box {
    /*//////////////////////////////////////////////////////////////
                                FUNCTIONS
    //////////////////////////////////////////////////////////////*/

    /// @notice Add an object to the box.
    function addOneObject() external {
        numberOfObjects += 1;
    }

    /// @notice Returns the box version.
    function boxVersion() external pure returns (uint256) {
        return 2;
    }
}        

  1. We’re inheriting the same logic from BoxV1.
  2. We added 2 new functions addOneObject() and boxVersion()

Writing an Upgrade?Script

In the scripts/ folder we will create a new file upgrade.ts and paste the following code:

import { ethers, defender } from "hardhat";

async function main() {
  const BoxV2 = await ethers.getContractFactory("BoxV2");

  const proposal = await defender.proposeUpgradeWithApproval(
    '[YOUR_PROXY_CONTRACT_ADDRESS]',
    BoxV2,
    {redeployImplementation: "always"}
  )

  console.log(`Upgrade proposed with URL: ${proposal.url}`);
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});        

  1. We’re creating a new proposal to upgrade the proxy logic to the new implementation (BoxV2).
  2. Make sure to replace [YOUR_PROXY_CONTRACT_ADDRESS] with your proxy contract address.

Running the Upgrade Sciprt and Submitting the?Proposal

Now, we will execute the following command in our Terminal:

npx hardhat run scripts/upgrade.ts --network goerli        

And then we should see the new proposal URL:

Executing the upgrade.ts script

In Defender we should see a new Upgrade proposal:

New Upgrade Proposal

We can click on it to enlarge it and then click “Open in Safe App”:

Upgrade Proposal Details

Approving the Upgrade Proposal in?Safe

Now in the Safe App, we will sign the transaction with 2 owners and execute it:

Approving the Proposal in Safe

After approving the proposal we can see that the upgrade was executed and now the proxy points to the new implementation (BoxV2.sol):

The Proxy Contract is Pointing to the New Implementation

Congratulation! We’ve successfully deployed and upgraded securely a smart contract utilizing both OpenZeppelin Defender and Safe!

The Deploy Module keeps a detailed history of all deployments. This history not only aids in tracking past deployments but also assists in identifying any potential issues or anomalies:

All deployments and upgrades history in one organized place

Accessing Defender?2.0

Defender 2.0 is currently in beta and available by invitation only. To gain access to this powerful platform, you can accelerate the process by signing up through the following link.

Mijo Grabovac

Blockchain/Solidity Developer | SmartContracts | Avalanche Subnets

1 年

Nice article, love your work! I just have a question in regards to a detail. Quote:"Deterministic Contract Deployment: When deploying across different EVM blockchains, you can ensure that the contracts have the same address on each chain. This is a vital feature to prevent cross-chain replay attacks " Are we sure this is correct? Cross-chain replay attack vulnerability are generally handled by the EIP-155 (using chainID in transaction hash). If anything, using same contact address on multiple chains would actually increase a risk of cross-chain replay attacks (if not for EIP-155). It's early morning for me here, so I might be missing something. But would love to learn more. Thanks again Johnny, love your work.

回复
Rishikesh Devghare

Full-Stack & Blockchain Developer | Quantum technology innovator | Multi-Disciplinary Artist

1 年

Wow this is huge!

Umar Khatab

I help companies build scalable Web3 products | Multichain Lending/Borrowing | DeFi | NFTs | EVM | Solana | Solidity | Foundry | Fuzz Testing | Invariant Testing | React.Js | Node.Js | Ethers.js | Web3 Security

1 年

Incredible content! Thank you johny

要查看或添加评论,请登录

Johnny Time的更多文章

社区洞察

其他会员也浏览了