diff --git a/hardhat.config.js b/hardhat.config.js index 9391dd7be..357a3013a 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -145,6 +145,18 @@ module.exports = { }, solidity: { compilers: [ + { + version: "0.8.9", + settings: { + optimizer: { + enabled: true, + runs: 200, + details: { + yul: true, + }, + }, + }, + }, { version: "0.8.18", settings: { diff --git a/test/upgrade/00_config.js b/test/upgrade/00_config.js index fbd6deaaa..5ebcdefe7 100644 --- a/test/upgrade/00_config.js +++ b/test/upgrade/00_config.js @@ -75,6 +75,7 @@ async function getFacets() { "v2.0.0": v2_0_0, "v2.1.0": v2_0_0, // same as v2.0.0 "v2.2.0": v2_2_0, + "v2.2.1": v2_2_0, // same as v2.2.0 }, upgrade: { "v2.1.0": { @@ -190,6 +191,7 @@ async function getFacets() { }, initializationData: "0x0000000000000000000000000000000000000000000000000000000000002710", // input for initV2_2_0, representing maxPremintedVoucher (0x2710=10000) }, + // POST 2.2.0 upgrade configs are part of respective migration script }, }; @@ -210,7 +212,11 @@ const tagsByVersion = { }, "2.2.1": { oldVersion: "v2.2.0", - newVersion: "v2.2.1-rc.1", + newVersion: "v2.2.1", + }, + "2.3.0": { + oldVersion: "v2.2.1", + newVersion: "v2.3.0", }, }; diff --git a/test/upgrade/2.2.1-2.3.0.js b/test/upgrade/2.2.1-2.3.0.js new file mode 100644 index 000000000..476465474 --- /dev/null +++ b/test/upgrade/2.2.1-2.3.0.js @@ -0,0 +1,280 @@ +const hre = require("hardhat"); +const { RevertReasons } = require("../../scripts/config/revert-reasons.js"); +const ethers = hre.ethers; +const { getSnapshot, revertToSnapshot } = require("../util/utils"); + +// const { getStateModifyingFunctionsHashes } = require("../../scripts/util/diamond-utils.js"); +const { assert, expect } = require("chai"); +const DisputeResolver = require("../../scripts/domain/DisputeResolver"); +const Seller = require("../../scripts/domain/Seller"); +const { calculateContractAddress } = require("../util/utils.js"); +const { mockSeller, mockAuthToken, mockVoucherInitValues } = require("../util/mock"); + +const { deploySuite, populateProtocolContract, getProtocolContractState, revertState } = require("../util/upgrade"); +const { getGenericContext } = require("./01_generic"); + +const version = "2.3.0"; +const { migrate } = require(`../../scripts/migrations/migrate_${version}.js`); + +/** + * Upgrade test case - After upgrade from 2.2.1 to 2.3.0 everything is still operational + */ +describe("[@skip-on-coverage] After facet upgrade, everything is still operational", function () { + // Common vars + let deployer, rando; + let accountHandler; + let snapshot; + let protocolDiamondAddress, mockContracts; + let contractsAfter; + let protocolContractStateBefore, protocolContractStateAfter; + let removedFunctionHashes, addedFunctionHashes; + + // reference protocol state + let accountContractState; + let preUpgradeEntities; + + before(async function () { + try { + // Make accounts available + [deployer, rando] = await ethers.getSigners(); + + let contractsBefore; + + ({ + protocolDiamondAddress, + protocolContracts: contractsBefore, + mockContracts, + } = await deploySuite(deployer, version)); + + // Populate protocol with data + preUpgradeEntities = await populateProtocolContract( + deployer, + protocolDiamondAddress, + contractsBefore, + mockContracts, + true + ); + + // Get current protocol state, which serves as the reference + // We assume that this state is a true one, relying on our unit and integration tests + protocolContractStateBefore = await getProtocolContractState( + protocolDiamondAddress, + contractsBefore, + mockContracts, + preUpgradeEntities, + true + ); + + ({ accountContractState } = protocolContractStateBefore); + + // const getFunctionHashesClosure = getStateModifyingFunctionsHashes( + // ["SellerHandlerFacet", "OrchestrationHandlerFacet1"], + // undefined, + // ["createSeller", "updateSeller"] + // ); + + // removedFunctionHashes = await getFunctionHashesClosure(); + + await migrate("upgrade-test"); + + // Cast to updated interface + let newHandlers = { + accountHandler: "IBosonAccountHandler", + orchestrationHandler: "IBosonOrchestrationHandler", + }; + + contractsAfter = { ...contractsBefore }; + + for (const [handlerName, interfaceName] of Object.entries(newHandlers)) { + contractsAfter[handlerName] = await ethers.getContractAt(interfaceName, protocolDiamondAddress); + } + + ({ accountHandler } = contractsAfter); + + // addedFunctionHashes = await getFunctionHashesClosure(); + + snapshot = await getSnapshot(); + + // const includeTests = [ + // "accountContractState", + // "offerContractState", + // "exchangeContractState", + // "bundleContractState", + // "configContractState", + // "disputeContractState", + // "fundsContractState", + // "groupContractState", + // "twinContractState", + // "protocolStatusPrivateContractState", + // "protocolLookupsPrivateContractState", + // ]; + + // Get protocol state after the upgrade + protocolContractStateAfter = await getProtocolContractState( + protocolDiamondAddress, + contractsAfter, + mockContracts, + preUpgradeEntities + ); + + // This context is placed in an uncommon place due to order of test execution. + // Generic context needs values that are set in "before", however "before" is executed before tests, not before suites + // and those values are undefined if this is placed outside "before". + // Normally, this would be solved with mocha's --delay option, but it does not behave as expected when running with hardhat. + context( + "Generic tests", + getGenericContext( + deployer, + protocolDiamondAddress, + contractsBefore, + contractsAfter, + mockContracts, + protocolContractStateBefore, + protocolContractStateAfter, + preUpgradeEntities, + snapshot + // includeTests + ) + ); + } catch (err) { + // revert to latest version of scripts and contracts + revertState(); + // stop execution + assert(false, `Before all reverts with: ${err}`); + } + }); + + afterEach(async function () { + // Revert to state right after the upgrade. + // This is used so the lengthy setup (deploy+upgrade) is done only once. + await revertToSnapshot(snapshot); + snapshot = await getSnapshot(); + }); + + after(async function () { + revertState(); + }); + + // To this test pass package.json version must be set + it(`Protocol status version is updated to ${version}`, async function () { + // Slice because of unicode escape notation + expect((await contractsAfter.protocolInitializationHandler.getVersion()).replace(/\0/g, "")).to.equal(version); + }); + + // Test actions that worked in previous version, but should not work anymore, or work differently + // Test methods that were added to see that upgrade was succesful + context.skip("📋 Breaking changes, new methods and bug fixes", async function () { + context("Breaking changes", async function () { + context("DisputeResolverHandlerFacet", async function () { + it("updateDisputeResolver reverts if no update field has been updated or requested to be updated", async function () { + const { DRs } = preUpgradeEntities; + const { wallet, id, disputeResolver } = DRs[0]; + + // Try to update with same values, should revert + await expect(accountHandler.connect(wallet).updateDisputeResolver(disputeResolver)).to.be.revertedWith( + RevertReasons.NO_UPDATE_APPLIED + ); + + // Validate if DR data is still the same + let [, disputeResolverAfter] = await accountHandler.getDisputeResolver(id); + disputeResolverAfter = DisputeResolver.fromStruct(disputeResolverAfter); + expect(disputeResolverAfter).to.deep.equal(disputeResolver); + }); + }); + + context("SellerHandlerFacet", async function () { + it("updateSeller reverts if no update field has been updated or requested to be updated", async function () { + const { sellers } = preUpgradeEntities; + const { wallet, id, seller, authToken } = sellers[0]; + + // Try to update with same values, should revert + await expect(accountHandler.connect(wallet).updateSeller(seller, authToken)).to.be.revertedWith( + RevertReasons.NO_UPDATE_APPLIED + ); + + // Validate if seller data is still the same + let [, sellerAfter] = await accountHandler.getSeller(id); + sellerAfter = Seller.fromStruct(sellerAfter); + expect(sellerAfter).to.deep.equal(seller); + }); + + it("Old seller can update and add metadataUri field", async function () { + const { sellers } = preUpgradeEntities; + const { wallet, id, seller, authToken } = sellers[0]; + + seller.metadataUri = "metadata"; + + const tx = await accountHandler.connect(wallet).updateSeller(seller, authToken); + expect(tx).to.emit("SellerUpdateApplied"); + + // Validate if seller now has metadataUri + let [, sellerAfter] = await accountHandler.getSeller(id); + sellerAfter = DisputeResolver.fromStruct(sellerAfter); + expect(sellerAfter.metadataUri).to.equal(seller.metadataUri); + }); + + it("New seller has metadataUri field", async function () { + const { nextAccountId } = accountContractState; + const seller = mockSeller(rando.address, rando.address, rando.address, rando.address, true, "metadata"); + const authToken = mockAuthToken(); + const voucherInitValues = mockVoucherInitValues(); + + const tx = await accountHandler.connect(rando).createSeller(seller, authToken, voucherInitValues); + expect(tx) + .to.emit("SellerCreated") + .withArgs( + nextAccountId, + seller, + calculateContractAddress(accountHandler.address, nextAccountId), + authToken, + rando.address + ); + }); + }); + + context("MetaTransactionHandlerfacet", async function () { + it("Function hashes from removedFunctionsHashes list should not be allowlisted", async function () { + for (const hash of removedFunctionHashes) { + const [isAllowed] = await contractsAfter.metaTransactionsHandler.functions[ + "isFunctionAllowlisted(bytes32)" + ](hash); + expect(isAllowed).to.be.false; + } + }); + + it("Function hashes from from addedFunctionsHashes list should be allowlisted", async function () { + for (const hash of addedFunctionHashes) { + const [isAllowed] = await contractsAfter.metaTransactionsHandler.functions[ + "isFunctionAllowlisted(bytes32)" + ](hash); + expect(isAllowed).to.be.true; + } + }); + + it("State of metaTxPrivateContractState is not affected besides isAllowlistedState mapping", async function () { + // make a shallow copy to not modify original protocolContractState as it's used on getGenericContext + const metaTxPrivateContractStateBefore = { ...protocolContractStateBefore.metaTxPrivateContractState }; + const metaTxPrivateContractStateAfter = { ...protocolContractStateAfter.metaTxPrivateContractState }; + + const { isAllowlistedState: isAllowlistedStateBefore } = metaTxPrivateContractStateBefore; + removedFunctionHashes.forEach((hash) => { + delete isAllowlistedStateBefore[hash]; + }); + + const { isAllowlistedState: isAllowlistedStateAfter } = metaTxPrivateContractStateAfter; + addedFunctionHashes.forEach((hash) => { + delete isAllowlistedStateAfter[hash]; + }); + + delete metaTxPrivateContractStateBefore.isAllowlistedState; + delete metaTxPrivateContractStateAfter.isAllowlistedState; + + expect(isAllowlistedStateAfter).to.deep.equal(isAllowlistedStateBefore); + expect(protocolContractStateAfter.metaTxPrivateContractState).to.deep.equal( + protocolContractStateBefore.metaTxPrivateContractState + ); + }); + }); + }); + }); +}); diff --git a/test/util/upgrade.js b/test/util/upgrade.js index 9979ef44c..6f9f2b5fe 100644 --- a/test/util/upgrade.js +++ b/test/util/upgrade.js @@ -89,6 +89,11 @@ async function deploySuite(deployer, newVersion) { shell.exec(`rm -rf scripts/*`); shell.exec(`git checkout ${scriptsTag} scripts`); } + const isOldOZVersion = ["v2.0", "v2.1", "v2.2"].some((v) => tag.startsWith(v)); + if (isOldOZVersion) { + // Temporary install old OZ contracts + shell.exec("npm i @openzeppelin/contracts-upgradeable@4.7.1"); + } const deployConfig = facets.deploy[tag]; @@ -148,6 +153,12 @@ async function deploySuite(deployer, newVersion) { await deployMockTokens(["Foreign20", "Foreign20", "Foreign721", "Foreign721", "Foreign20", "Foreign1155"]); const mockTwinTokens = [mockTwin721_1, mockTwin721_2]; + if (isOldOZVersion) { + // If reference commit is old version, we need to revert to target version + shell.exec(`git checkout ${versionTags.newVersion} package.json package-lock.json`); + shell.exec("npm i"); + } + return { protocolDiamondAddress, protocolContracts: {