Skip to content

Commit

Permalink
Merge pull request #655 from NomicFoundation/upgradeable-example-project
Browse files Browse the repository at this point in the history
Add sample project for upgradeable proxy usage
  • Loading branch information
zoeyTM authored Jun 20, 2024
2 parents 1675490 + d9fc7a8 commit c4278b6
Show file tree
Hide file tree
Showing 14 changed files with 1,125 additions and 902 deletions.
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
```

## 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;
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

0 comments on commit c4278b6

Please sign in to comment.