Deploy Upgradable Smart Contract Securely with Defender 2.0 by OpenZeppelin
Johnny Time
Founder @ Ginger Security | Blockchain Security Engineer and Web3 Security Educator. Learn more at: johnnytime.xyz
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.
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.
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:
Integration with MultiSig (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:
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:
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:
Now select the network you want to deploy your contracts to, in my example, I selected 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:
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:
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:
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:
The next time you click the “deploy” module you will see that your setup 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:
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;
In the root folder, we are going to create a?.env file and store there the Defender’s apiKey and apiSecret:
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;
});
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:
Also, if we go to the Defender dashboard we will see the new deployment as well:
领英推荐
We can always click on it to get extra 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:
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:
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”:
Now complete the following information and click “Save Changes”:
Head back to the Deploy page, and click “Configure” on the Goerli Network:
Now you can select your approval process and click “Save Changes”:
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 {}
}
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;
});
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:
Also, if we go to the Defender dashboard we will see two new smart contracts, the Box Implementation and en ERC1976 proxy:
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;
}
}
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;
});
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:
In Defender we should see a new Upgrade proposal:
We can click on it to enlarge it and then click “Open in Safe App”:
Approving the Upgrade Proposal in?Safe
Now in the Safe App, we will sign the transaction with 2 owners and execute it:
After approving the proposal we can see that the upgrade was executed and now the proxy points to the new implementation (BoxV2.sol):
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:
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.
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.
Full-Stack & Blockchain Developer | Quantum technology innovator | Multi-Disciplinary Artist
1 年Wow this is huge!
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