Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add sample project for upgradeable proxy usage #655

Merged
merged 10 commits into from
Jun 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions examples/upgradeable/.eslintrc.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
module.exports = {
extends: ["plugin:prettier/recommended"],
parserOptions: {
ecmaVersion: "latest",
},
env: {
es6: true,
node: true,
},
rules: {
"no-console": "error",
},
ignorePatterns: [".eslintrc.js", "artifacts/*", "cache/*"],
};
13 changes: 13 additions & 0 deletions examples/upgradeable/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
node_modules
.env
coverage
coverage.json
typechain
typechain-types

#Hardhat files
cache
artifacts

ignition/deployments

5 changes: 5 additions & 0 deletions examples/upgradeable/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
/node_modules
/artifacts
/cache
/coverage
/.nyc_output
25 changes: 25 additions & 0 deletions examples/upgradeable/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Upgradeable Contract Example for Hardhat Ignition

This project is a basic example of how to use Hardhat Ignition with contract systems that use an upgradeable proxy pattern.

## Deploying

To deploy the an example proxy contract against the ephemeral Hardhat network:

```shell
npx hardhat ignition deploy ./ignition/modules/ProxyModule.js
```

To deploy an example of a proxy contract being upgraded against the ephemeral Hardhat network:

```shell
npx hardhat ignition deploy ./ignition/modules/UpgradeModule.js
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I split these two calls apart @zoeyTM

```

## Test

To run the Hardhat tests using Ignition:

```shell
npm run test
```
9 changes: 9 additions & 0 deletions examples/upgradeable/contracts/Demo.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// A contrived example of a contract that can be upgraded
contract Demo {
function version() public pure returns (string memory) {
return "1.0.0";
}
}
15 changes: 15 additions & 0 deletions examples/upgradeable/contracts/DemoV2.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// A contrived example of a contract that can be upgraded
contract DemoV2 {
string public name;

function version() public pure returns (string memory) {
return "2.0.0";
}

function setName(string memory _name) public {
name = _name;
}
}
7 changes: 7 additions & 0 deletions examples/upgradeable/contracts/Proxies.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.9;

// We import these here to force Hardhat to compile them.
// This ensures that their artifacts are available for Hardhat Ignition to use.
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
7 changes: 7 additions & 0 deletions examples/upgradeable/hardhat.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
require("@nomicfoundation/hardhat-toolbox");
require("@nomicfoundation/hardhat-ignition-ethers");

/** @type import('hardhat/config').HardhatUserConfig */
module.exports = {
solidity: "0.8.20",
};
59 changes: 59 additions & 0 deletions examples/upgradeable/ignition/modules/ProxyModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

/**
* This is the first module that will be run. It deploys the proxy and the
* proxy admin, and returns them so that they can be used by other modules.
*/
const proxyModule = buildModule("ProxyModule", (m) => {
// This address is the owner of the ProxyAdmin contract,
// so it will be the only account that can upgrade the proxy when needed.
const proxyAdminOwner = m.getAccount(0);

// This is our contract that will be proxied.
// We will upgrade this contract with a new version later.
const demo = m.contract("Demo");

// The TransparentUpgradeableProxy contract creates the ProxyAdmin within its constructor.
// To read more about how this proxy is implemented, you can view the source code and comments here:
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol
const proxy = m.contract("TransparentUpgradeableProxy", [
demo,
proxyAdminOwner,
"0x",
]);

// We need to get the address of the ProxyAdmin contract that was created by the TransparentUpgradeableProxy
// so that we can use it to upgrade the proxy later.
const proxyAdminAddress = m.readEventArgument(
proxy,
"AdminChanged",
"newAdmin"
);

// Here we use m.contractAt(...) to create a contract instance for the ProxyAdmin that we can interact with later to upgrade the proxy.
const proxyAdmin = m.contractAt("ProxyAdmin", proxyAdminAddress);

// Return the proxy and proxy admin so that they can be used by other modules.
return { proxyAdmin, proxy };
});

/**
* This is the second module that will be run, and it is also the only module exported from this file.
* It creates a contract instance for the Demo contract using the proxy from the previous module.
*/
const demoModule = buildModule("DemoModule", (m) => {
// Get the proxy and proxy admin from the previous module.
const { proxy, proxyAdmin } = m.useModule(proxyModule);

// Here we're using m.contractAt(...) a bit differently than we did above.
// While we're still using it to create a contract instance, we're now telling Hardhat Ignition
// to treat the contract at the proxy address as an instance of the Demo contract.
// This allows us to interact with the underlying Demo contract via the proxy from within tests and scripts.
const demo = m.contractAt("Demo", proxy);

// Return the contract instance, along with the original proxy and proxyAdmin contracts
// so that they can be used by other modules, or in tests and scripts.
return { demo, proxy, proxyAdmin };
});

module.exports = demoModule;
zoeyTM marked this conversation as resolved.
Show resolved Hide resolved
57 changes: 57 additions & 0 deletions examples/upgradeable/ignition/modules/UpgradeModule.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");

const ProxyModule = require("./ProxyModule");

/**
* This module upgrades the proxy to a new version of the Demo contract.
*/
const upgradeModule = buildModule("UpgradeModule", (m) => {
// Make sure we're using the account that owns the ProxyAdmin contract.
const proxyAdminOwner = m.getAccount(0);

// Get the proxy and proxy admin from the previous module.
const { proxyAdmin, proxy } = m.useModule(ProxyModule);

// This is the new version of the Demo contract that we want to upgrade to.
const demoV2 = m.contract("DemoV2");

// The `upgradeAndCall` function on the ProxyAdmin contract allows us to upgrade the proxy
// and call a function on the new implementation contract in a single transaction.
// To do this, we need to encode the function call data for the function we want to call.
// We'll then pass this encoded data to the `upgradeAndCall` function.
const encodedFunctionCall = m.encodeFunctionCall(demoV2, "setName", [
"Example Name",
]);

// Upgrade the proxy to the new version of the Demo contract.
// This function also accepts a data parameter, which accepts encoded function call data.
// We pass the encoded function call data we created above to the `upgradeAndCall` function
// so that the `setName` function is called on the new implementation contract after the upgrade.
m.call(proxyAdmin, "upgradeAndCall", [proxy, demoV2, encodedFunctionCall], {
from: proxyAdminOwner,
});

// Return the proxy and proxy admin so that they can be used by other modules.
return { proxyAdmin, proxy };
});

/**
* This is the final module that will be run.
*
* It takes the proxy from the previous module and uses it to create a local contract instance
* for the DemoV2 contract. This allows us to interact with the DemoV2 contract via the proxy.
*/
const demoV2Module = buildModule("DemoV2Module", (m) => {
// Get the proxy from the previous module.
const { proxy } = m.useModule(upgradeModule);

// Create a local contract instance for the DemoV2 contract.
// This line tells Hardhat Ignition to use the DemoV2 ABI for the contract at the proxy address.
// This allows us to call functions on the DemoV2 contract via the proxy.
const demo = m.contractAt("DemoV2", proxy);

// Return the contract instance so that it can be used by other modules or in tests.
return { demo };
});

module.exports = demoV2Module;
21 changes: 21 additions & 0 deletions examples/upgradeable/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"name": "@nomicfoundation/ignition-upgradeable-example",
"private": true,
"version": "0.15.2",
"scripts": {
"test": "hardhat test",
"lint": "npm run prettier -- --check && npm run eslint",
"lint:fix": "npm run prettier -- --write && npm run eslint -- --fix",
"eslint": "eslint \"ignition/**/*.{js,jsx}\" \"test/**/*.{js,jsx}\"",
"prettier": "prettier \"*.{js,md,json}\" \"ignition/modules/*.{js,md,json}\" \"test/*.{js,md,json}\" \"contracts/**/*.sol\""
},
"devDependencies": {
"@nomicfoundation/hardhat-ignition-ethers": "workspace:^",
"@nomicfoundation/hardhat-toolbox": "4.0.0",
"hardhat": "^2.18.0",
"prettier-plugin-solidity": "1.1.3"
},
"dependencies": {
"@openzeppelin/contracts": "^5.0.1"
}
}
34 changes: 34 additions & 0 deletions examples/upgradeable/test/ProxyDemo.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
const { expect } = require("chai");

const ProxyModule = require("../ignition/modules/ProxyModule");
const UpgradeModule = require("../ignition/modules/UpgradeModule");

describe("Demo Proxy", function () {
describe("Proxy interaction", async function () {
it("Should be interactable via proxy", async function () {
const [, otherAccount] = await ethers.getSigners();

const { demo } = await ignition.deploy(ProxyModule);

expect(await demo.connect(otherAccount).version()).to.equal("1.0.0");
});
});

describe("Upgrading", function () {
it("Should have upgraded the proxy to DemoV2", async function () {
const [, otherAccount] = await ethers.getSigners();

const { demo } = await ignition.deploy(UpgradeModule);

expect(await demo.connect(otherAccount).version()).to.equal("2.0.0");
});

it("Should have set the name during upgrade", async function () {
const [, otherAccount] = await ethers.getSigners();

const { demo } = await ignition.deploy(UpgradeModule);

expect(await demo.connect(otherAccount).name()).to.equal("Example Name");
});
});
});
Loading
Loading