Skip to content

Commit c4278b6

Browse files
authored
Merge pull request #655 from NomicFoundation/upgradeable-example-project
Add sample project for upgradeable proxy usage
2 parents 1675490 + d9fc7a8 commit c4278b6

14 files changed

+1125
-902
lines changed

examples/upgradeable/.eslintrc.js

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module.exports = {
2+
extends: ["plugin:prettier/recommended"],
3+
parserOptions: {
4+
ecmaVersion: "latest",
5+
},
6+
env: {
7+
es6: true,
8+
node: true,
9+
},
10+
rules: {
11+
"no-console": "error",
12+
},
13+
ignorePatterns: [".eslintrc.js", "artifacts/*", "cache/*"],
14+
};

examples/upgradeable/.gitignore

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
node_modules
2+
.env
3+
coverage
4+
coverage.json
5+
typechain
6+
typechain-types
7+
8+
#Hardhat files
9+
cache
10+
artifacts
11+
12+
ignition/deployments
13+

examples/upgradeable/.prettierignore

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/node_modules
2+
/artifacts
3+
/cache
4+
/coverage
5+
/.nyc_output

examples/upgradeable/README.md

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# Upgradeable Contract Example for Hardhat Ignition
2+
3+
This project is a basic example of how to use Hardhat Ignition with contract systems that use an upgradeable proxy pattern.
4+
5+
## Deploying
6+
7+
To deploy the an example proxy contract against the ephemeral Hardhat network:
8+
9+
```shell
10+
npx hardhat ignition deploy ./ignition/modules/ProxyModule.js
11+
```
12+
13+
To deploy an example of a proxy contract being upgraded against the ephemeral Hardhat network:
14+
15+
```shell
16+
npx hardhat ignition deploy ./ignition/modules/UpgradeModule.js
17+
```
18+
19+
## Test
20+
21+
To run the Hardhat tests using Ignition:
22+
23+
```shell
24+
npm run test
25+
```
+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.9;
3+
4+
// A contrived example of a contract that can be upgraded
5+
contract Demo {
6+
function version() public pure returns (string memory) {
7+
return "1.0.0";
8+
}
9+
}
+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.9;
3+
4+
// A contrived example of a contract that can be upgraded
5+
contract DemoV2 {
6+
string public name;
7+
8+
function version() public pure returns (string memory) {
9+
return "2.0.0";
10+
}
11+
12+
function setName(string memory _name) public {
13+
name = _name;
14+
}
15+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
// SPDX-License-Identifier: UNLICENSED
2+
pragma solidity ^0.8.9;
3+
4+
// We import these here to force Hardhat to compile them.
5+
// This ensures that their artifacts are available for Hardhat Ignition to use.
6+
import "@openzeppelin/contracts/proxy/transparent/ProxyAdmin.sol";
7+
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
require("@nomicfoundation/hardhat-toolbox");
2+
require("@nomicfoundation/hardhat-ignition-ethers");
3+
4+
/** @type import('hardhat/config').HardhatUserConfig */
5+
module.exports = {
6+
solidity: "0.8.20",
7+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
2+
3+
/**
4+
* This is the first module that will be run. It deploys the proxy and the
5+
* proxy admin, and returns them so that they can be used by other modules.
6+
*/
7+
const proxyModule = buildModule("ProxyModule", (m) => {
8+
// This address is the owner of the ProxyAdmin contract,
9+
// so it will be the only account that can upgrade the proxy when needed.
10+
const proxyAdminOwner = m.getAccount(0);
11+
12+
// This is our contract that will be proxied.
13+
// We will upgrade this contract with a new version later.
14+
const demo = m.contract("Demo");
15+
16+
// The TransparentUpgradeableProxy contract creates the ProxyAdmin within its constructor.
17+
// To read more about how this proxy is implemented, you can view the source code and comments here:
18+
// https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.0.1/contracts/proxy/transparent/TransparentUpgradeableProxy.sol
19+
const proxy = m.contract("TransparentUpgradeableProxy", [
20+
demo,
21+
proxyAdminOwner,
22+
"0x",
23+
]);
24+
25+
// We need to get the address of the ProxyAdmin contract that was created by the TransparentUpgradeableProxy
26+
// so that we can use it to upgrade the proxy later.
27+
const proxyAdminAddress = m.readEventArgument(
28+
proxy,
29+
"AdminChanged",
30+
"newAdmin"
31+
);
32+
33+
// Here we use m.contractAt(...) to create a contract instance for the ProxyAdmin that we can interact with later to upgrade the proxy.
34+
const proxyAdmin = m.contractAt("ProxyAdmin", proxyAdminAddress);
35+
36+
// Return the proxy and proxy admin so that they can be used by other modules.
37+
return { proxyAdmin, proxy };
38+
});
39+
40+
/**
41+
* This is the second module that will be run, and it is also the only module exported from this file.
42+
* It creates a contract instance for the Demo contract using the proxy from the previous module.
43+
*/
44+
const demoModule = buildModule("DemoModule", (m) => {
45+
// Get the proxy and proxy admin from the previous module.
46+
const { proxy, proxyAdmin } = m.useModule(proxyModule);
47+
48+
// Here we're using m.contractAt(...) a bit differently than we did above.
49+
// While we're still using it to create a contract instance, we're now telling Hardhat Ignition
50+
// to treat the contract at the proxy address as an instance of the Demo contract.
51+
// This allows us to interact with the underlying Demo contract via the proxy from within tests and scripts.
52+
const demo = m.contractAt("Demo", proxy);
53+
54+
// Return the contract instance, along with the original proxy and proxyAdmin contracts
55+
// so that they can be used by other modules, or in tests and scripts.
56+
return { demo, proxy, proxyAdmin };
57+
});
58+
59+
module.exports = demoModule;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const { buildModule } = require("@nomicfoundation/hardhat-ignition/modules");
2+
3+
const ProxyModule = require("./ProxyModule");
4+
5+
/**
6+
* This module upgrades the proxy to a new version of the Demo contract.
7+
*/
8+
const upgradeModule = buildModule("UpgradeModule", (m) => {
9+
// Make sure we're using the account that owns the ProxyAdmin contract.
10+
const proxyAdminOwner = m.getAccount(0);
11+
12+
// Get the proxy and proxy admin from the previous module.
13+
const { proxyAdmin, proxy } = m.useModule(ProxyModule);
14+
15+
// This is the new version of the Demo contract that we want to upgrade to.
16+
const demoV2 = m.contract("DemoV2");
17+
18+
// The `upgradeAndCall` function on the ProxyAdmin contract allows us to upgrade the proxy
19+
// and call a function on the new implementation contract in a single transaction.
20+
// To do this, we need to encode the function call data for the function we want to call.
21+
// We'll then pass this encoded data to the `upgradeAndCall` function.
22+
const encodedFunctionCall = m.encodeFunctionCall(demoV2, "setName", [
23+
"Example Name",
24+
]);
25+
26+
// Upgrade the proxy to the new version of the Demo contract.
27+
// This function also accepts a data parameter, which accepts encoded function call data.
28+
// We pass the encoded function call data we created above to the `upgradeAndCall` function
29+
// so that the `setName` function is called on the new implementation contract after the upgrade.
30+
m.call(proxyAdmin, "upgradeAndCall", [proxy, demoV2, encodedFunctionCall], {
31+
from: proxyAdminOwner,
32+
});
33+
34+
// Return the proxy and proxy admin so that they can be used by other modules.
35+
return { proxyAdmin, proxy };
36+
});
37+
38+
/**
39+
* This is the final module that will be run.
40+
*
41+
* It takes the proxy from the previous module and uses it to create a local contract instance
42+
* for the DemoV2 contract. This allows us to interact with the DemoV2 contract via the proxy.
43+
*/
44+
const demoV2Module = buildModule("DemoV2Module", (m) => {
45+
// Get the proxy from the previous module.
46+
const { proxy } = m.useModule(upgradeModule);
47+
48+
// Create a local contract instance for the DemoV2 contract.
49+
// This line tells Hardhat Ignition to use the DemoV2 ABI for the contract at the proxy address.
50+
// This allows us to call functions on the DemoV2 contract via the proxy.
51+
const demo = m.contractAt("DemoV2", proxy);
52+
53+
// Return the contract instance so that it can be used by other modules or in tests.
54+
return { demo };
55+
});
56+
57+
module.exports = demoV2Module;

examples/upgradeable/package.json

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@nomicfoundation/ignition-upgradeable-example",
3+
"private": true,
4+
"version": "0.15.2",
5+
"scripts": {
6+
"test": "hardhat test",
7+
"lint": "npm run prettier -- --check && npm run eslint",
8+
"lint:fix": "npm run prettier -- --write && npm run eslint -- --fix",
9+
"eslint": "eslint \"ignition/**/*.{js,jsx}\" \"test/**/*.{js,jsx}\"",
10+
"prettier": "prettier \"*.{js,md,json}\" \"ignition/modules/*.{js,md,json}\" \"test/*.{js,md,json}\" \"contracts/**/*.sol\""
11+
},
12+
"devDependencies": {
13+
"@nomicfoundation/hardhat-ignition-ethers": "workspace:^",
14+
"@nomicfoundation/hardhat-toolbox": "4.0.0",
15+
"hardhat": "^2.18.0",
16+
"prettier-plugin-solidity": "1.1.3"
17+
},
18+
"dependencies": {
19+
"@openzeppelin/contracts": "^5.0.1"
20+
}
21+
}
+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
const { expect } = require("chai");
2+
3+
const ProxyModule = require("../ignition/modules/ProxyModule");
4+
const UpgradeModule = require("../ignition/modules/UpgradeModule");
5+
6+
describe("Demo Proxy", function () {
7+
describe("Proxy interaction", async function () {
8+
it("Should be interactable via proxy", async function () {
9+
const [, otherAccount] = await ethers.getSigners();
10+
11+
const { demo } = await ignition.deploy(ProxyModule);
12+
13+
expect(await demo.connect(otherAccount).version()).to.equal("1.0.0");
14+
});
15+
});
16+
17+
describe("Upgrading", function () {
18+
it("Should have upgraded the proxy to DemoV2", async function () {
19+
const [, otherAccount] = await ethers.getSigners();
20+
21+
const { demo } = await ignition.deploy(UpgradeModule);
22+
23+
expect(await demo.connect(otherAccount).version()).to.equal("2.0.0");
24+
});
25+
26+
it("Should have set the name during upgrade", async function () {
27+
const [, otherAccount] = await ethers.getSigners();
28+
29+
const { demo } = await ignition.deploy(UpgradeModule);
30+
31+
expect(await demo.connect(otherAccount).name()).to.equal("Example Name");
32+
});
33+
});
34+
});

0 commit comments

Comments
 (0)