-
Notifications
You must be signed in to change notification settings - Fork 28
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
Changes from all commits
Commits
Show all changes
10 commits
Select commit
Hold shift + click to select a range
d462b7f
add upgradeable proxy sample project
zoeyTM a1ee429
add comments to the upgradeable proxy example
zoeyTM 3c1295a
update readme
zoeyTM 776d8f5
refactor upgrade example
zoeyTM 4f5055f
update upgradeable example dependency version
zoeyTM dbc14fd
add encodeFunctionCall to upgrade example
zoeyTM b673177
chore: update upgradeable to latest version
kanej 76aa65a
chore: update pnpm lock
kanej b80febf
refactor: clean up test
kanej d9fc7a8
docs: tweak example commmand text
kanej File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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; | ||
zoeyTM marked this conversation as resolved.
Show resolved
Hide resolved
|
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.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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