diff --git a/examples/upgradeable/contracts/Box.sol b/examples/upgradeable/contracts/Box.sol deleted file mode 100644 index 0c6cd7998..000000000 --- a/examples/upgradeable/contracts/Box.sol +++ /dev/null @@ -1,20 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -contract Box { - uint256 private _value; - - // Emitted when the stored value changes - event ValueChanged(uint256 value); - - // Stores a new value in the contract - function store(uint256 value) public { - _value = value; - emit ValueChanged(value); - } - - // Reads the last stored value - function retrieve() public view returns (uint256) { - return _value; - } -} diff --git a/examples/upgradeable/contracts/Demo.sol b/examples/upgradeable/contracts/Demo.sol new file mode 100644 index 000000000..e30433e4c --- /dev/null +++ b/examples/upgradeable/contracts/Demo.sol @@ -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"; + } +} diff --git a/examples/upgradeable/contracts/DemoV2.sol b/examples/upgradeable/contracts/DemoV2.sol new file mode 100644 index 000000000..1d5494c66 --- /dev/null +++ b/examples/upgradeable/contracts/DemoV2.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.9; + +// A contrived example of a contract that can be upgraded +contract DemoV2 { + function version() public pure returns (string memory) { + return "2.0.0"; + } +} diff --git a/examples/upgradeable/contracts/Proxies.sol b/examples/upgradeable/contracts/Proxies.sol index ee3734a87..4c4e8c723 100644 --- a/examples/upgradeable/contracts/Proxies.sol +++ b/examples/upgradeable/contracts/Proxies.sol @@ -1,5 +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"; diff --git a/examples/upgradeable/contracts/UpgradedBox.sol b/examples/upgradeable/contracts/UpgradedBox.sol deleted file mode 100644 index 645e8744d..000000000 --- a/examples/upgradeable/contracts/UpgradedBox.sol +++ /dev/null @@ -1,24 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.9; - -contract UpgradedBox { - uint256 private _value; - - // Emitted when the stored value changes - event ValueChanged(uint256 value); - - // Stores a new value in the contract - function store(uint256 value) public { - _value = value; - emit ValueChanged(value); - } - - // Reads the last stored value - function retrieve() public view returns (uint256) { - return _value; - } - - function version() public pure returns (string memory) { - return "2.0.0"; - } -} diff --git a/examples/upgradeable/ignition/modules/ProxyModule.js b/examples/upgradeable/ignition/modules/ProxyModule.js index c33ddcf36..805c75291 100644 --- a/examples/upgradeable/ignition/modules/ProxyModule.js +++ b/examples/upgradeable/ignition/modules/ProxyModule.js @@ -1,48 +1,85 @@ // ./ignition/LockModule.js 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); - const box = m.contract("Box"); + // 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", [ - box, + demo, proxyAdminOwner, "0x", ]); + // We need to get the address of the ProxyAdmin contrac 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. 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. It upgrades the proxy to a new + * version of the Demo contract. + */ const upgradeModule = buildModule("UpgradeModule", (m) => { + // Make sure we're 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); - const upgradedBox = m.contract("UpgradedBox"); + // This is the new version of the Demo contract that we want to upgrade to. + const demoV2 = m.contract("DemoV2"); - m.call(proxyAdmin, "upgradeAndCall", [proxy, upgradedBox, "0x"], { + // Upgrade the proxy to the new version of the Demo contract. + // This function also accepts a data parameter, which can be used to call a function, + // but we don't need it here so we pass an empty hex string ("0x"). + m.call(proxyAdmin, "upgradeAndCall", [proxy, demoV2, "0x"], { from: proxyAdminOwner, }); + // Return the proxy and proxy admin so that they can be used by other modules. return { proxyAdmin, proxy }; }); +/** + * This is the third and 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 interactableModule = buildModule("InteractableModule", (m) => { + // Get the proxy from the previous module. const { proxy } = m.useModule(upgradeModule); - const box = m.contractAt("UpgradedBox", proxy); + // Create a local contract instance for the DemoV2 contract. + // This line tells Hardhat Ignition to treat the contract at the proxy address as an DemoV2 contract. + // This allows us to call functions on the DemoV2 contract via the proxy. + const demo = m.contractAt("DemoV2", proxy); - return { box }; + // Return the contract instance so that it can be used by other modules or in tests. + return { demo }; }); module.exports = interactableModule; diff --git a/examples/upgradeable/test/BoxProxy.js b/examples/upgradeable/test/BoxProxy.js deleted file mode 100644 index ad5b49afd..000000000 --- a/examples/upgradeable/test/BoxProxy.js +++ /dev/null @@ -1,23 +0,0 @@ -const { - loadFixture, -} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); -const { expect } = require("chai"); -const ProxyModule = require("../ignition/modules/ProxyModule"); - -describe("UpgradedBox", function () { - async function deployFixture() { - const [owner, otherAccount] = await ethers.getSigners(); - - const { box } = await ignition.deploy(ProxyModule); - - return { box, owner, otherAccount }; - } - - describe("Upgrading", function () { - it("Should have upgraded the proxy to UpgradedBox", async function () { - const { box, otherAccount } = await loadFixture(deployFixture); - - expect(await box.connect(otherAccount).version()).to.equal("2.0.0"); - }); - }); -}); diff --git a/examples/upgradeable/test/ProxyDemo.js b/examples/upgradeable/test/ProxyDemo.js new file mode 100644 index 000000000..a375120a4 --- /dev/null +++ b/examples/upgradeable/test/ProxyDemo.js @@ -0,0 +1,24 @@ +const { + loadFixture, +} = require("@nomicfoundation/hardhat-toolbox/network-helpers"); +const { expect } = require("chai"); + +const ProxyModule = require("../ignition/modules/ProxyModule"); + +describe("Demo Proxy", function () { + async function deployFixture() { + const [owner, otherAccount] = await ethers.getSigners(); + + const { demo } = await ignition.deploy(ProxyModule); + + return { demo, owner, otherAccount }; + } + + describe("Upgrading", function () { + it("Should have upgraded the proxy to DemoV2", async function () { + const { demo, otherAccount } = await loadFixture(deployFixture); + + expect(await demo.connect(otherAccount).version()).to.equal("2.0.0"); + }); + }); +});