-
Notifications
You must be signed in to change notification settings - Fork 28
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #655 from NomicFoundation/upgradeable-example-project
Add sample project for upgradeable proxy usage
- Loading branch information
Showing
14 changed files
with
1,125 additions
and
902 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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/*"], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
/node_modules | ||
/artifacts | ||
/cache | ||
/coverage | ||
/.nyc_output |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.