diff --git a/contracts/protocol/facets/DisputeResolverHandlerFacet.sol b/contracts/protocol/facets/DisputeResolverHandlerFacet.sol index ca9d9be61..2bac3ebef 100644 --- a/contracts/protocol/facets/DisputeResolverHandlerFacet.sol +++ b/contracts/protocol/facets/DisputeResolverHandlerFacet.sol @@ -641,6 +641,7 @@ contract DisputeResolverHandlerFacet is IBosonAccountEvents, ProtocolBase { { (exists, disputeResolver, disputeResolverFees) = fetchDisputeResolver(_disputeResolverId); if (exists) { + disputeResolver.clerk = address(0); sellerAllowList = protocolLookups().allowedSellers[_disputeResolverId]; } } diff --git a/contracts/protocol/facets/ExchangeHandlerFacet.sol b/contracts/protocol/facets/ExchangeHandlerFacet.sol index 829fc48f8..585e92c2d 100644 --- a/contracts/protocol/facets/ExchangeHandlerFacet.sol +++ b/contracts/protocol/facets/ExchangeHandlerFacet.sol @@ -870,7 +870,6 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { // Token transfer order is descending twinM.tokenId += twinM.supplyAvailable; } - // ERC-721 style transfer data = abi.encodeWithSignature( "safeTransferFrom(address,address,uint256,bytes)", @@ -890,7 +889,6 @@ contract ExchangeHandlerFacet is IBosonExchangeHandler, BuyerBase, DisputeBase { "" ); } - // Make call only if there is enough gas and code at address exists. // If not, skip the call and mark the transfer as failed twinM.tokenAddress = twinS.tokenAddress; diff --git a/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol b/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol index 88e2120c9..0e15675ca 100644 --- a/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol +++ b/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol @@ -155,6 +155,7 @@ contract ProtocolInitializationHandlerFacet is IBosonProtocolInitializationHandl function initV2_3_0(bytes calldata _initializationData) internal { // Current version must be 2.2.1 require(protocolStatus().version == bytes32("2.2.1"), WRONG_CURRENT_VERSION); + require(protocolCounters().nextTwinId == 1, TWINS_ALREADY_EXIST); // Decode initialization data diff --git a/contracts/protocol/facets/SellerHandlerFacet.sol b/contracts/protocol/facets/SellerHandlerFacet.sol index a9ac322de..452185063 100644 --- a/contracts/protocol/facets/SellerHandlerFacet.sol +++ b/contracts/protocol/facets/SellerHandlerFacet.sol @@ -496,7 +496,7 @@ contract SellerHandlerFacet is SellerBase { (exists, sellerId) = getSellerIdByAuthToken(_associatedAuthToken); if (exists) { - return fetchSeller(sellerId); + return fetchSellerWithoutClerk(sellerId); } } diff --git a/hardhat.config.js b/hardhat.config.js index 979028bee..0e62aeb54 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -53,11 +53,12 @@ task( task("upgrade-facets", "Upgrade existing facets, add new facets or remove existing facets") .addParam("newVersion", "The version of the protocol to upgrade to") .addParam("env", "The deployment environment") + .addParam("functionNamesToSelector", "JSON list of function names to selectors") .addOptionalParam("facetConfig", "JSON list of facets to upgrade") - .setAction(async ({ env, facetConfig, newVersion }) => { + .setAction(async ({ env, facetConfig, newVersion, functionNamesToSelector }) => { const { upgradeFacets } = await lazyImport("./scripts/upgrade-facets.js"); - await upgradeFacets(env, facetConfig, newVersion); + await upgradeFacets(env, facetConfig, newVersion, functionNamesToSelector); }); task("upgrade-clients", "Upgrade existing clients") @@ -115,9 +116,9 @@ task("migrate", "Migrates the protocol to a new version") if (dryRun) { const balanceAfter = await getBalance(); - const etherSpent = balanceBefore.sub(balanceAfter); + const etherSpent = balanceBefore - balanceAfter; - const formatUnits = require("ethers").utils.formatUnits; + const formatUnits = require("ethers").formatUnits; console.log("Ether spent: ", formatUnits(etherSpent, "ether")); } }); diff --git a/package.json b/package.json index ccbeb79c8..b80d56a4e 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "tidy:contracts": "solhint --fix contracts/**/*.sol && prettier --write contracts/**", "tidy:scripts": "eslint --fix test/** scripts/** '*.js' && prettier --write test/** scripts/** '*.js'", "size": "npx hardhat size-contracts", - "coverage": "npx hardhat clean && npx hardhat coverage", + "coverage": "npx hardhat clean && npx hardhat coverage --testfiles '{test/access/*.js,test/protocol/*.js,test/protocol/clients/*.js}'", "deploy-suite:hardhat": "npx hardhat clean && npx hardhat compile && npx hardhat deploy-suite --network hardhat", "deploy-suite:local": "npx hardhat clean && npx hardhat compile && npx hardhat deploy-suite --network localhost", "deploy-suite:test": "npx hardhat clean && npx hardhat compile && npx hardhat deploy-suite --network test --env test >> logs/test.deploy.contracts.txt", @@ -52,6 +52,7 @@ "upgrade-facets:polygon:mumbai-test": "npx hardhat clean && npx hardhat compile && npx hardhat upgrade-facets --network mumbai --env test >> logs/mumbai-test.upgrade.contracts.txt", "upgrade-facets:polygon:mumbai-staging": "npx hardhat clean && npx hardhat compile && npx hardhat upgrade-facets --network mumbai --env staging >> logs/mumbai-staging.upgrade.contracts.txt", "upgrade-facets:polygon:mainnet": "npx hardhat clean && npx hardhat compile && npx hardhat upgrade-facets --network polygon --env prod >> logs/polygon.upgrade.contracts.txt", + "migrate": "npx hardhat clean && npx hardhat compile && npx hardhat migrate >> logs/${npm_config_env}-migrate.contracts.txt", "upgrade-clients:local": "npx hardhat clean && npx hardhat compile && npx hardhat upgrade-clients --network localhost --env ''", "upgrade-clients:test": "npx hardhat clean && npx hardhat compile && npx hardhat upgrade-clients --network test --env test >> logs/test.upgrade.contracts.txt", "upgrade-clients:ethereum:mainnet": "npx hardhat clean && npx hardhat compile && npx hardhat upgrade-clients --network mainnet --env prod >> logs/mainnet.upgrade.contracts.txt", diff --git a/scripts/domain/Offer.js b/scripts/domain/Offer.js index 1e020a565..323963a41 100644 --- a/scripts/domain/Offer.js +++ b/scripts/domain/Offer.js @@ -115,6 +115,9 @@ class Offer { voided, collectionIndex, ] = struct; + if (!collectionIndex) { + collectionIndex = 0; + } return Offer.fromObject({ id: id.toString(), diff --git a/scripts/manage-roles.js b/scripts/manage-roles.js index 8ca209e5f..38e2d7a84 100644 --- a/scripts/manage-roles.js +++ b/scripts/manage-roles.js @@ -2,7 +2,7 @@ const hre = require("hardhat"); const { getContractAt, provider } = hre.ethers; const network = hre.network.name; const { RoleAssignments } = require("./config/role-assignments"); -const { readContracts } = require("./util/utils"); +const { readContracts, listAccounts } = require("./util/utils"); const environments = require("../environments"); const Role = require("./domain/Role"); @@ -39,7 +39,7 @@ async function main(env) { console.log(`ā›“ Network: ${hre.network.name}\nšŸ“… ${new Date()}`); // Get the accounts - const accounts = await provider.listAccounts(); + const accounts = await listAccounts(); const admin = accounts[0]; console.log("šŸ”± Admin account: ", admin ? admin : "not found" && process.exit()); console.log(divider); diff --git a/scripts/migrations/dry-run.js b/scripts/migrations/dry-run.js index 796edf6e3..14495ea6b 100644 --- a/scripts/migrations/dry-run.js +++ b/scripts/migrations/dry-run.js @@ -29,7 +29,7 @@ async function setupDryRun(env) { await hre.changeNetwork("hardhat"); - env = "upgrade-test"; + env = `${env}-dry-run`; const { chainId } = await ethers.provider.getNetwork(); if (chainId != "31337") process.exit(1); // make sure network is hardhat diff --git a/scripts/migrations/migrate_2.2.1.js b/scripts/migrations/migrate_2.2.1.js index fc989b090..1d819ef55 100644 --- a/scripts/migrations/migrate_2.2.1.js +++ b/scripts/migrations/migrate_2.2.1.js @@ -41,6 +41,7 @@ async function migrate(env) { console.log("Compiling old contracts"); await hre.run("clean"); + await hre.run("compile"); const { chainId } = await provider.getNetwork(); diff --git a/scripts/migrations/migrate_2.3.0.js b/scripts/migrations/migrate_2.3.0.js index d8d45b6ca..fb818db7e 100644 --- a/scripts/migrations/migrate_2.3.0.js +++ b/scripts/migrations/migrate_2.3.0.js @@ -1,96 +1,237 @@ const shell = require("shelljs"); -const { readContracts } = require("../util/utils.js"); + +const { getStateModifyingFunctionsHashes, getSelectors } = require("../../scripts/util/diamond-utils.js"); +const environments = require("../../environments"); +const Role = require("../domain/Role"); +const tipMultiplier = BigInt(environments.tipMultiplier); +const tipSuggestion = 1500000000n; // js always returns this constant, it does not vary per block +const maxPriorityFeePerGas = tipSuggestion + tipMultiplier; +const { readContracts, getFees, checkRole } = require("../util/utils.js"); const hre = require("hardhat"); +const { oneWeek } = require("../../test/util/constants.js"); +const PausableRegion = require("../domain/PausableRegion.js"); const ethers = hre.ethers; +const { getContractAt, getSigners } = ethers; const network = hre.network.name; -// const { getStateModifyingFunctionsHashes } = require("../../scripts/util/diamond-utils.js"); +const abiCoder = new ethers.AbiCoder(); const tag = "HEAD"; const version = "2.3.0"; +const { EXCHANGE_ID_2_2_0 } = require("../config/protocol-parameters"); +const { META_TRANSACTION_FORWARDER } = require("../config/client-upgrade"); +const confirmations = hre.network.name == "hardhat" ? 1 : environments.confirmations; const config = { // status at 451dc3d. ToDo: update this to the latest commit addOrUpgrade: [ + "ConfigHandlerFacet", "DisputeResolverHandlerFacet", + "ExchangeHandlerFacet", "FundsHandlerFacet", "MetaTransactionsHandlerFacet", "OfferHandlerFacet", "OrchestrationHandlerFacet1", + "PauseHandlerFacet", + "DisputeHandlerFacet", "ProtocolInitializationHandlerFacet", "SellerHandlerFacet", + "BundleHandlerFacet", "TwinHandlerFacet", + "GroupHandlerFacet", ], remove: [], skipSelectors: {}, - facetsToInit: {}, - initializationData: "0x", + facetsToInit: { + ExchangeHandlerFacet: { init: [], constructorArgs: [EXCHANGE_ID_2_2_0[network]] }, + }, // must match nextExchangeId at the time of the upgrade + initializationData: abiCoder.encode(["uint256", "uint256[]", "address[]"], [oneWeek, [], []]), }; async function migrate(env) { console.log(`Migration ${tag} started`); try { - console.log("Removing any local changes before upgrading"); - shell.exec(`git reset @{u}`); - const statusOutput = shell.exec("git status -s -uno scripts"); - - if (statusOutput.stdout) { - throw new Error("Local changes found. Please stash them before upgrading"); + if (env != "upgrade-test") { + console.log("Removing any local changes before upgrading"); + shell.exec(`git reset @{u}`); + const statusOutput = shell.exec("git status -s -uno scripts package.json"); + + if (statusOutput.stdout) { + throw new Error("Local changes found. Please stash them before upgrading"); + } } const { chainId } = await ethers.provider.getNetwork(); const contractsFile = readContracts(chainId, network, env); + if (contractsFile?.protocolVersion != "2.2.1") { throw new Error("Current contract version must be 2.2.1"); } - console.log("Installing dependencies"); - shell.exec(`npm install`); - - // let contracts = contractsFile?.contracts; + let contracts = contractsFile?.contracts; // Get addresses of currently deployed contracts - // const protocolAddress = contracts.find((c) => c.name === "ProtocolDiamond")?.address; + const accessControllerAddress = contracts.find((c) => c.name === "AccessController")?.address; - // Checking old version contracts to get selectors to remove - // ToDo: at 451dc3d, no selectors to remove. Comment out this section. It will be needed when other changes are merged into main - // console.log("Checking out contracts on version 2.2.1"); - // shell.exec(`rm -rf contracts/*`); - // shell.exec(`git checkout v2.2.1 contracts`); + const accessController = await getContractAt("AccessController", accessControllerAddress); - // console.log("Compiling old contracts"); - // await hre.run("clean"); - // await hre.run("compile"); + const signer = (await getSigners())[0].address; + if (env == "upgrade-test") { + // Grant PAUSER role to the deployer + await accessController.grantRole(Role.PAUSER, signer); + } else { + checkRole(contracts, "PAUSER", signer); + } + + const protocolAddress = contracts.find((c) => c.name === "ProtocolDiamond")?.address; + + console.log("Pausing the Seller region..."); + let pauseHandler = await getContractAt("IBosonPauseHandler", protocolAddress); + const pauseTransaction = await pauseHandler.pause([PausableRegion.Sellers], await getFees(maxPriorityFeePerGas)); + + // await 1 block to ensure the pause is effective + await pauseTransaction.wait(confirmations); + + if (env != "upgrade-test") { + // Checking old version contracts to get selectors to remove + console.log("Checking out contracts on version 2.2.1"); + shell.exec(`rm -rf contracts/*`); + shell.exec(`git checkout v2.2.1 contracts package.json package-lock.json`); + console.log("Installing dependencies"); + shell.exec("npm install"); + console.log("Compiling old contracts"); + await hre.run("clean"); + await hre.run("compile"); + } - // const getFunctionHashesClosure = getStateModifyingFunctionsHashes( - // ["SellerHandlerFacet", "OrchestrationHandlerFacet1"], - // undefined, - // ["createSeller", "updateSeller"] - // ); + // Get the list of creators and their ids + config.initializationData = abiCoder.encode( + ["uint256"], + [oneWeek] // ToDo <- from config? + ); + console.log("Initialization data: ", config.initializationData); + + let functionNamesToSelector = {}; + + for (const facet of config.addOrUpgrade) { + const facetContract = await getContractAt(facet, protocolAddress); + const { signatureToNameMapping } = getSelectors(facetContract, true); + functionNamesToSelector = { ...functionNamesToSelector, ...signatureToNameMapping }; + } - // const selectorsToRemove = await getFunctionHashesClosure(); + let getFunctionHashesClosure = getStateModifyingFunctionsHashes( + [ + "SellerHandlerFacet", + "OfferHandlerFacet", + "ConfigHandlerFacet", + "PauseHandlerFacet", + "GroupHandlerFacet", + "OrchestrationHandlerFacet1", + ], + undefined, + [ + "createSeller", + "createOffer", + "createPremintedOffer", + "unpause", + "createGroup", + "setGroupCondition", + "setMaxOffersPerBatch", + "setMaxOffersPerGroup", + "setMaxTwinsPerBundle", + "setMaxOffersPerBundle", + "setMaxTokensPerWithdrawal", + "setMaxFeesPerDisputeResolver", + "setMaxDisputesPerBatch", + "setMaxAllowedSellers", + "setMaxExchangesPerBatch", + "setMaxPremintedVouchers", + ] + ); + + const selectorsToRemove = await getFunctionHashesClosure(); console.log(`Checking out contracts on version ${tag}`); shell.exec(`rm -rf contracts/*`); - shell.exec(`git checkout ${tag} contracts`); + shell.exec(`git checkout ${tag} contracts package.json package-lock.json`); + + shell.exec(`git checkout HEAD scripts`); + + console.log("Installing dependencies"); + shell.exec(`npm install`); console.log("Compiling contracts"); await hre.run("clean"); - await hre.run("compile"); + // If some contract was removed, compilation succeeds, but afterwards it falsely reports missing artifacts + // This is a workaround to ignore the error + try { + await hre.run("compile"); + } catch {} console.log("Executing upgrade facets script"); await hre.run("upgrade-facets", { env, facetConfig: JSON.stringify(config), newVersion: version, + functionNamesToSelector: JSON.stringify(functionNamesToSelector), }); - // const selectorsToAdd = await getFunctionHashesClosure(); + getFunctionHashesClosure = getStateModifyingFunctionsHashes( + [ + "SellerHandlerFacet", + "OfferHandlerFacet", + "ConfigHandlerFacet", + "PauseHandlerFacet", + "GroupHandlerFacet", + "OrchestrationHandlerFacet1", + "ExchangeHandlerFacet", + ], + undefined, + [ + "createSeller", + "createOffer", + "createPremintedOffer", + "unpause", + "createGroup", + "setGroupCondition", + "createNewCollection", + "setMinResolutionPeriod", + "commitToConditionalOffer", + "updateSellerSalt", + ] + ); + + const selectorsToAdd = await getFunctionHashesClosure(); + const metaTransactionHandlerFacet = await getContractAt("MetaTransactionsHandlerFacet", protocolAddress); + console.log("Removing selectors", selectorsToRemove.join(",")); + await metaTransactionHandlerFacet.setAllowlistedFunctions(selectorsToRemove, false); + + // check if functions were removed + for (const selector of selectorsToRemove) { + const isFunctionAllowlisted = await metaTransactionHandlerFacet.getFunction("isFunctionAllowlisted(bytes32)"); + const isAllowed = await isFunctionAllowlisted.staticCall(selector); + if (isAllowed) { + console.error(`Selector ${selector} was not removed`); + } + } + + console.log("Adding selectors", selectorsToAdd.join(",")); + await metaTransactionHandlerFacet.setAllowlistedFunctions(selectorsToAdd, true); - // const metaTransactionHandlerFacet = await ethers.getContractAt("MetaTransactionsHandlerFacet", protocolAddress); + console.log("Executing upgrade clients script"); + + const clientConfig = { + META_TRANSACTION_FORWARDER, + }; + + // Upgrade clients + await hre.run("upgrade-clients", { + env, + clientConfig: JSON.stringify(clientConfig), + newVersion: version, + }); - // console.log("Removing selectors", selectorsToRemove.join(",")); - // await metaTransactionHandlerFacet.setAllowlistedFunctions(selectorsToRemove, false); - // console.log("Adding selectors", selectorsToAdd.join(",")); - // await metaTransactionHandlerFacet.setAllowlistedFunctions(selectorsToAdd, true); + console.log("Unpausing all regions..."); + pauseHandler = await getContractAt("IBosonPauseHandler", protocolAddress); + await pauseHandler.unpause([], await getFees(maxPriorityFeePerGas)); shell.exec(`git checkout HEAD`); console.log(`Migration ${tag} completed`); diff --git a/scripts/upgrade-clients.js b/scripts/upgrade-clients.js index df0549401..059d6798d 100644 --- a/scripts/upgrade-clients.js +++ b/scripts/upgrade-clients.js @@ -3,8 +3,8 @@ const { provider, ZeroAddress, getSigners, getSigner, getContractAt } = hre.ethe const network = hre.network.name; const environments = require("../environments"); const tipMultiplier = BigInt(environments.tipMultiplier); -const tipSuggestion = "1500000000"; // js always returns this constant, it does not vary per block -const maxPriorityFeePerGas = BigInt(tipSuggestion).mul(tipMultiplier); +const tipSuggestion = 1500000000n; // js always returns this constant, it does not vary per block +const maxPriorityFeePerGas = tipSuggestion * tipMultiplier; const { deploymentComplete, getFees, @@ -12,9 +12,9 @@ const { writeContracts, checkRole, addressNotFound, + listAccounts, } = require("./util/utils.js"); const { deployProtocolClientImpls } = requireUncached("./util/deploy-protocol-client-impls.js"); -const Role = require("./domain/Role"); /** * Upgrades clients @@ -27,7 +27,7 @@ const Role = require("./domain/Role"); */ async function main(env, clientConfig, version) { // Bail now if hardhat network, unless the upgrade is tested - if (network === "hardhat" && env !== "upgrade-test") process.exit(); + if (network === "hardhat" && env !== "upgrade-test" && !env.includes("dry-run")) process.exit(); const { chainId } = await provider.getNetwork(); let { contracts } = readContracts(chainId, network, env); @@ -46,7 +46,7 @@ async function main(env, clientConfig, version) { } // Get list of accounts managed by node - const nodeAccountList = (await provider.listAccounts()).map((address) => address.toLowerCase()); + const nodeAccountList = (await listAccounts()).map((address) => address.toLowerCase()); if (nodeAccountList.includes(adminAddress.toLowerCase())) { console.log("šŸ”± Admin account: ", adminAddress); @@ -66,7 +66,7 @@ async function main(env, clientConfig, version) { } // Validate that admin has UPGRADER role - checkRole(contracts, Role.UPGRADER, adminAddress); + checkRole(contracts, "UPGRADER", adminAddress); clientConfig = (clientConfig && JSON.parse(clientConfig)) || require("./config/client-upgrade"); diff --git a/scripts/upgrade-facets.js b/scripts/upgrade-facets.js index 3dbfad08d..0092e4487 100644 --- a/scripts/upgrade-facets.js +++ b/scripts/upgrade-facets.js @@ -7,7 +7,14 @@ const environments = require("../environments"); const tipMultiplier = BigInt(environments.tipMultiplier); const tipSuggestion = 1500000000n; // js always returns this constant, it does not vary per block const maxPriorityFeePerGas = tipSuggestion + tipMultiplier; -const { deploymentComplete, readContracts, writeContracts, checkRole, addressNotFound } = require("./util/utils.js"); +const { + deploymentComplete, + readContracts, + writeContracts, + checkRole, + addressNotFound, + listAccounts, +} = require("./util/utils.js"); const { deployProtocolFacets } = requireUncached("./util/deploy-protocol-handler-facets.js"); const { FacetCutAction, @@ -17,7 +24,6 @@ const { getInitializeCalldata, } = require("./util/diamond-utils.js"); const { getInterfaceIds, interfaceImplementers } = require("./config/supported-interfaces.js"); -const Role = require("./domain/Role"); const packageFile = require("../package.json"); const readline = require("readline"); const FacetCut = require("./domain/FacetCut"); @@ -40,9 +46,9 @@ const rl = readline.createInterface({ * 2. Run the appropriate npm script in package.json to upgrade facets for a given network * 3. Save changes to the repo as a record of what was upgraded */ -async function main(env, facetConfig, version) { +async function main(env, facetConfig, version, functionNamesToSelector) { // Bail now if hardhat network, unless the upgrade is tested - if (network === "hardhat" && env !== "upgrade-test") process.exit(); + if (network === "hardhat" && env !== "upgrade-test" && !env.includes("dry-run")) process.exit(); const { chainId } = await provider.getNetwork(); const contractsFile = readContracts(chainId, network, env); @@ -90,7 +96,7 @@ async function main(env, facetConfig, version) { } // Get list of accounts managed by node - const nodeAccountList = (await provider.listAccounts()).map((address) => address.toLowerCase()); + const nodeAccountList = (await listAccounts()).map((address) => address.toLowerCase()); if (nodeAccountList.includes(adminAddress.toLowerCase())) { console.log("šŸ”± Admin account: ", adminAddress); @@ -104,7 +110,7 @@ async function main(env, facetConfig, version) { const protocolAddress = contracts.find((c) => c.name === "ProtocolDiamond")?.address; // Check if admin has UPGRADER role - checkRole(contracts, Role.UPGRADER, adminAddress); + checkRole(contracts, "UPGRADER", adminAddress); if (!protocolAddress) { return addressNotFound("ProtocolDiamond"); @@ -138,6 +144,8 @@ async function main(env, facetConfig, version) { interfacesToAdd = {}; const removedSelectors = []; // global list of selectors to be removed + functionNamesToSelector = JSON.parse(functionNamesToSelector); + // Remove facets for (const facetToRemove of facets.remove) { // Get currently registered selectors @@ -146,7 +154,7 @@ async function main(env, facetConfig, version) { let registeredSelectors; if (oldFacet) { // Facet exists, so all selectors should be removed - registeredSelectors = await diamondLoupe.facetFunctionSelectors(await oldFacet.getAddress()); + registeredSelectors = await diamondLoupe.facetFunctionSelectors(oldFacet.address); } else { // Facet does not exist, skip next steps continue; @@ -188,9 +196,10 @@ async function main(env, facetConfig, version) { // Get currently registered selectors const oldFacet = contracts.find((i) => i.name === newFacet.name); let registeredSelectors; + if (oldFacet) { // Facet already exists and is only upgraded - registeredSelectors = await diamondLoupe.facetFunctionSelectors(await oldFacet.getAddress()); + registeredSelectors = await diamondLoupe.facetFunctionSelectors(oldFacet.address); } else { // Facet is new registeredSelectors = []; @@ -200,39 +209,47 @@ async function main(env, facetConfig, version) { contracts = contracts.filter((i) => i.name !== newFacet.name); const newFacetInterfaceId = interfaceIdFromFacetName(newFacet.name); - deploymentComplete(newFacet.name, newFacet.contract, newFacet.constructorArgs, newFacetInterfaceId, contracts); + deploymentComplete( + newFacet.name, + await newFacet.contract.getAddress(), + newFacet.constructorArgs, + newFacetInterfaceId, + contracts + ); // Get new selectors from compiled contract - const selectors = getSelectors(newFacet.contract, true); - let newSelectors = selectors.selectors; + let { selectors: newSelectors, signatureToNameMapping } = getSelectors(newFacet.contract, true); + functionNamesToSelector = { ...functionNamesToSelector, ...signatureToNameMapping }; // Initialize selectors should not be added const facetFactory = await getContractFactory(newFacet.name); - const functionFragment = facetFactory.interface.getFunction("initialize"); - const signature = facetFactory.interface.getSighash(functionFragment); - newSelectors = selectors.selectors.remove([signature]); + const { selector } = facetFactory.interface.getFunction("initialize"); + newSelectors = newSelectors.remove([selector]); // Determine actions to be made - let selectorsToReplace = registeredSelectors.filter((value) => newSelectors.includes(value)); // intersection of old and new selectors + let selectorsToReplace = registeredSelectors.filter((value) => newSelectors.includes(value)); let selectorsToRemove = registeredSelectors.filter((value) => !selectorsToReplace.includes(value)); // unique old selectors let selectorsToAdd = newSelectors.filter((value) => !selectorsToReplace.includes(value)); // unique new selectors // Skip selectors if set in config let selectorsToSkip = facets.skipSelectors[newFacet.name] ? facets.skipSelectors[newFacet.name] : []; selectorsToReplace = removeSelectors(selectorsToReplace, selectorsToSkip); + selectorsToRemove = removeSelectors(selectorsToRemove, selectorsToSkip); + selectorsToAdd = removeSelectors(selectorsToAdd, selectorsToSkip); // Check if selectors that are being added are not registered yet on some other facet // If collision is found, user must choose to either (s)kip it or (r)eplace it. let skipAll, replaceAll; + for (const selectorToAdd of selectorsToAdd) { if (removedSelectors.flat().includes(selectorToAdd)) continue; // skip if selector is already marked for removal from another facet const existingFacetAddress = await diamondLoupe.facetAddress(selectorToAdd); if (existingFacetAddress != ZeroAddress) { // Selector exist on some other facet - const selectorName = selectors.signatureToNameMapping[selectorToAdd]; + const selectorName = signatureToNameMapping[selectorToAdd]; let answer; if (!(skipAll || replaceAll)) { const prompt = `Selector ${selectorName} is already registered on facet ${existingFacetAddress}. Do you want to (r)eplace or (s)kip it?\nUse "R" os "S" to apply the same choice to all remaining selectors in this facet. `; @@ -255,15 +272,16 @@ async function main(env, facetConfig, version) { } } - const newFacetAddress = newFacet.contract; + const newFacetAddress = await newFacet.contract.getAddress(); + if (selectorsToAdd.length > 0) { - deployedFacets[index].cut.push([newFacetAddress, FacetCutAction.Add, selectorsToAdd]); + deployedFacets[index].cut.push([newFacetAddress, FacetCutAction.Add, [...selectorsToAdd]]); } if (selectorsToReplace.length > 0) { - deployedFacets[index].cut.push([newFacetAddress, FacetCutAction.Replace, selectorsToReplace]); + deployedFacets[index].cut.push([newFacetAddress, FacetCutAction.Replace, [...selectorsToReplace]]); } if (selectorsToRemove.length > 0) { - deployedFacets[index].cut.push([ZeroAddress, FacetCutAction.Remove, selectorsToRemove]); + deployedFacets[index].cut.push([ZeroAddress, FacetCutAction.Remove, [...selectorsToRemove]]); } if (oldFacet && (selectorsToAdd.length > 0 || selectorsToRemove.length > 0)) { @@ -328,8 +346,7 @@ async function main(env, facetConfig, version) { return facetCut.toObject(); }); - const selectors = getSelectors(facet.contract, true); - logFacetCut(cut, selectors); + logFacetCut(cut, functionNamesToSelector); } console.log(`\nšŸ’€ Removed facets:\n\t${facets.remove.join("\n\t")}`); @@ -411,14 +428,14 @@ const getInitializationFacet = async (deployedFacets, contracts) => { return protocolInitializationFacet; }; -const logFacetCut = (cut, selectors) => { +const logFacetCut = (cut, functionNamesToSelector) => { for (const action in FacetCutAction) { cut .filter((c) => c.action == FacetCutAction[action]) .forEach((c) => { console.log( `šŸ’Ž ${action} selectors:\n\t${c.functionSelectors - .map((selector) => `${selectors.signatureToNameMapping[selector]}: ${selector}`) + .map((selector) => `${functionNamesToSelector[selector]}: ${selector}`) .join("\n\t")}` ); }); diff --git a/scripts/upgrade-hooks/2.2.0.js b/scripts/upgrade-hooks/2.2.0.js index b439d59bb..614b77230 100644 --- a/scripts/upgrade-hooks/2.2.0.js +++ b/scripts/upgrade-hooks/2.2.0.js @@ -2,8 +2,8 @@ const { ethers } = require("hardhat"); const { getContractAt } = ethers; const environments = require("../../environments"); const tipMultiplier = BigInt(environments.tipMultiplier); -const tipSuggestion = "1500000000"; // js always returns this constant, it does not vary per block -const maxPriorityFeePerGas = BigInt(tipSuggestion).mul(tipMultiplier); +const tipSuggestion = 1500000000n; // js always returns this constant, it does not vary per block +const maxPriorityFeePerGas = BigInt(tipSuggestion) * tipMultiplier; const { getFees } = require("./../util/utils"); const PausableRegion = require("../domain/PausableRegion.js"); diff --git a/scripts/util/deploy-mock-tokens.js b/scripts/util/deploy-mock-tokens.js index b42d11ffe..af30e9cb4 100644 --- a/scripts/util/deploy-mock-tokens.js +++ b/scripts/util/deploy-mock-tokens.js @@ -1,9 +1,10 @@ const hre = require("hardhat"); const { expect } = require("chai"); const environments = require("../../environments"); -const { getContractFactory, provider, ZeroAddress, getAddress } = hre.ethers; +const { getContractFactory, ZeroAddress, getAddress } = hre.ethers; const network = hre.network.name; const confirmations = hre.network.name == "hardhat" ? 1 : environments.confirmations; +const { listAccounts } = require("./utils"); /** * Deploy mock tokens for unit tests @@ -57,7 +58,7 @@ async function deployAndMintMockNFTAuthTokens() { console.log("\n Tokens will be minted to addresses ", addresses); } } else if (network == "hardhat") { - [...addresses] = await provider.listAccounts(); + addresses = await listAccounts(); //We only need auth tokens for 3 addresses addresses.splice(3, 18); diff --git a/scripts/util/diamond-utils.js b/scripts/util/diamond-utils.js index 1262fc48e..b0e6e5e15 100644 --- a/scripts/util/diamond-utils.js +++ b/scripts/util/diamond-utils.js @@ -150,11 +150,12 @@ async function getStateModifyingFunctions(facetNames, omitFunctions = [], onlyFu const functions = FacetContractFactory.interface.fragments; const facetStateModifyingFunctions = functions .filter((fn) => { - if (fn.type == "function" && fn.stateMutability !== "view" && !omitFunctions.includes(fn.name)) { + if (fn.type == "function" && fn.stateMutability !== "view" && !omitFunctions.some((f) => fn.name.includes(f))) { if (onlyFunctions.length === 0) { return true; } - if (onlyFunctions.includes(fn.name)) { + + if (onlyFunctions.some((f) => fn.name.includes(f))) { return true; } } diff --git a/scripts/util/utils.js b/scripts/util/utils.js index 3cf7b0fb1..639ddcac9 100644 --- a/scripts/util/utils.js +++ b/scripts/util/utils.js @@ -2,6 +2,7 @@ const hre = require("hardhat"); const { provider, getContractAt } = hre.ethers; const fs = require("fs"); const addressesDirPath = __dirname + `/../../addresses`; +const Role = require("./../domain/Role"); function getAddressesFilePath(chainId, network, env) { return `${addressesDirPath}/${chainId}${network ? `-${network.toLowerCase()}` : ""}${env ? `-${env}` : ""}.json`; @@ -12,7 +13,6 @@ function delay(ms) { } function deploymentComplete(name, address, args, interfaceId, contracts) { - console.log("address", address); contracts.push({ name, address, args, interfaceId }); console.log(`āœ… ${name} deployed to: ${address}`); } @@ -67,10 +67,10 @@ async function checkRole(contracts, role, address) { // Get AccessController abstraction const accessController = await getContractAt("AccessController", accessControllerAddress); - // Check that caller has upgrader role. - const hasRole = await accessController.hasRole(role, address); + // Check that caller has specified role. + const hasRole = await accessController.hasRole(Role[role], address); if (!hasRole) { - console.log("Admin address does not have UPGRADER role"); + console.log(`Admin address does not have ${role} role`); process.exit(1); } } @@ -83,6 +83,11 @@ function toHexString(bigNumber, { startPad } = { startPad: 8 }) { return "0x" + (startPad ? bigNumber.toString(16).padStart(startPad, "0") : bigNumber.toString(16)); } +// Workaround since hardhat provider doesn't support listAccounts yet (this may be a hardhat bug after ether v6 migration) +async function listAccounts() { + return await provider.send("eth_accounts", []); +} + exports.getAddressesFilePath = getAddressesFilePath; exports.writeContracts = writeContracts; exports.readContracts = readContracts; @@ -92,3 +97,4 @@ exports.getFees = getFees; exports.checkRole = checkRole; exports.addressNotFound = addressNotFound; exports.toHexString = toHexString; +exports.listAccounts = listAccounts; diff --git a/test/protocol/SellerHandlerTest.js b/test/protocol/SellerHandlerTest.js index 36f673f48..191576136 100644 --- a/test/protocol/SellerHandlerTest.js +++ b/test/protocol/SellerHandlerTest.js @@ -849,7 +849,7 @@ describe("SellerHandler", function () { seller.assistant = await rando.getAddress(); seller.clerk = await rando.getAddress(); - // Attempt to Create a seller with clerk not the same to caller address + // Attempt to Create a seller with clerk not 0 await expect( accountHandler.connect(rando).createSeller(seller, emptyAuthToken, voucherInitValues) ).to.revertedWith(RevertReasons.CLERK_DEPRECATED); diff --git a/test/upgrade/00_config.js b/test/upgrade/00_config.js index edbcce0e5..c75265d5f 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 }, }; @@ -212,6 +214,11 @@ const tagsByVersion = { oldVersion: "v2.2.0", newVersion: "v2.2.1", }, + "2.3.0": { + oldVersion: "v2.2.1", + newVersion: "v2.3.0", + updateDomain: ["Condition"], + }, }; exports.getFacets = getFacets; diff --git a/test/upgrade/01_generic.js b/test/upgrade/01_generic.js index ed86a62ea..1c04d63b0 100644 --- a/test/upgrade/01_generic.js +++ b/test/upgrade/01_generic.js @@ -19,20 +19,7 @@ function getGenericContext( protocolContractStateAfterUpgrade, preUpgradeEntities, snapshot, - includeTests = [ - "accountContractState", - "offerContractState", - "exchangeContractState", - "bundleContractState", - "configContractState", - "disputeContractState", - "fundsContractState", - "groupContractState", - "twinContractState", - "metaTxPrivateContractState", - "protocolStatusPrivateContractState", - "protocolLookupsPrivateContractState", - ] + includeTests ) { let postUpgradeEntities; let { exchangeHandler, offerHandler, fundsHandler, disputeHandler } = contractsBefore; @@ -41,7 +28,7 @@ function getGenericContext( const genericContextFunction = async function () { afterEach(async function () { // Revert to state right after the upgrade. - // This is used so the lengthly setup (deploy+upgrade) is done only once. + // This is used so the lengthy setup (deploy+upgrade) is done only once. await revertToSnapshot(snapshot); snapshot = await getSnapshot(); }); @@ -55,7 +42,7 @@ function getGenericContext( // Protocol state context("šŸ“‹ Right After upgrade", async function () { - for (const test in includeTests) { + for (const test of includeTests) { it(`State of ${test} is not affected`, async function () { assert.deepEqual(protocolContractState[test], protocolContractStateAfterUpgrade[test]); }); @@ -64,6 +51,7 @@ function getGenericContext( // Create new protocol entities. Existing data should not be affected context("šŸ“‹ New data after the upgrade do not corrupt the data from before the upgrade", async function () { + this.timeout(1000000); let protocolContractStateAfterUpgradeAndActions; before(async function () { @@ -227,7 +215,7 @@ function getGenericContext( const seller = preUpgradeEntities.sellers.find((s) => s.seller.id == offer.offer.sellerId); await expect(exchangeHandler.connect(seller.wallet).revokeVoucher(exchange.exchangeId)) .to.emit(exchangeHandler, "VoucherRevoked") - .withArgs(exchange.offerId, exchange.exchangeId, seller.wallet); + .withArgs(exchange.offerId, exchange.exchangeId, seller.wallet.address); }); it("Escalate old dispute", async function () { @@ -235,6 +223,7 @@ function getGenericContext( const buyerWallet = preUpgradeEntities.buyers[exchange.buyerIndex].wallet; const offer = preUpgradeEntities.offers.find((o) => o.offer.id == exchange.offerId); + await expect(disputeHandler.connect(buyerWallet).escalateDispute(exchange.exchangeId)) .to.emit(disputeHandler, "DisputeEscalated") .withArgs(exchange.exchangeId, offer.disputeResolverId, await buyerWallet.getAddress()); @@ -251,6 +240,8 @@ function getGenericContext( const disputeResolverId = preUpgradeEntities.DRs[0].disputeResolver.id; const agentId = preUpgradeEntities.agents[0].agent.id; const seller = preUpgradeEntities.sellers[2]; + + offerHandler = contractsAfter.offerHandler; await offerHandler .connect(seller.wallet) .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); @@ -309,7 +300,7 @@ function getGenericContext( await expect(offerHandler.connect(seller.wallet).voidOffer(offerId)) .to.emit(offerHandler, "OfferVoided") - .withArgs(offerId, seller.seller.id, seller.wallet); + .withArgs(offerId, seller.seller.id, seller.wallet.address); }); }); }; 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..1b6a7173a --- /dev/null +++ b/test/upgrade/2.2.1-2.3.0.js @@ -0,0 +1,1611 @@ +const shell = require("shelljs"); +const hre = require("hardhat"); +const ethers = hre.ethers; +const { ZeroAddress, parseEther, Wallet, provider, getContractFactory, getContractAt, encodeBytes32String } = ethers; +const { assert, expect } = require("chai"); +const { anyValue } = require("@nomicfoundation/hardhat-chai-matchers/withArgs"); + +const { RevertReasons } = require("../../scripts/config/revert-reasons.js"); +const SellerUpdateFields = require("../../scripts/domain/SellerUpdateFields"); +const DisputeResolverUpdateFields = require("../../scripts/domain/DisputeResolverUpdateFields"); +const PausableRegion = require("../../scripts/domain/PausableRegion.js"); +const Role = require("../../scripts/domain/Role"); +const AuthToken = require("../../scripts/domain/AuthToken"); +const AuthTokenType = require("../../scripts/domain/AuthTokenType"); +const Bundle = require("../../scripts/domain/Bundle"); +const ExchangeState = require("../../scripts/domain/ExchangeState"); +const Group = require("../../scripts/domain/Group"); +const TokenType = require("../../scripts/domain/TokenType.js"); +const EvaluationMethod = require("../../scripts/domain/EvaluationMethod"); +const { Collection, CollectionList } = require("../../scripts/domain/Collection"); +const { DisputeResolverFee } = require("../../scripts/domain/DisputeResolverFee"); + +const { FundsList } = require("../../scripts/domain/Funds"); +const { getStateModifyingFunctionsHashes } = require("../../scripts/util/diamond-utils.js"); +const { toHexString } = require("../../scripts/util/utils.js"); +const { + mockSeller, + mockAuthToken, + mockVoucherInitValues, + mockDisputeResolver, + mockOffer, + mockTwin, + mockCondition, + accountId, +} = require("../util/mock"); +const { + getSnapshot, + revertToSnapshot, + setNextBlockTimestamp, + getEvent, + calculateCloneAddress, + deriveTokenId, + calculateBosonProxyAddress, +} = require("../util/utils"); +const { limits: protocolLimits } = require("../../scripts/config/protocol-parameters.js"); + +const { + deploySuite, + populateProtocolContract, + getProtocolContractState, + revertState, + getStorageLayout, + getVoucherContractState, + populateVoucherContract, +} = require("../util/upgrade"); +const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens"); +const { getGenericContext } = require("./01_generic"); +const { getGenericContext: getGenericContextVoucher } = require("./clients/01_generic"); +const { oneWeek, oneMonth, VOUCHER_NAME, VOUCHER_SYMBOL } = require("../util/constants"); +const GatingType = require("../../scripts/domain/GatingType.js"); + +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 () { + this.timeout(1000000); + // Common vars + let deployer, rando, clerk, pauser, assistant; + let accessController; + let accountHandler, + fundsHandler, + pauseHandler, + configHandler, + offerHandler, + bundleHandler, + exchangeHandler, + twinHandler, + disputeHandler, + groupHandler, + orchestrationHandler; + let snapshot; + let protocolDiamondAddress, mockContracts; + let contractsAfter; + let protocolContractStateBefore, protocolContractStateAfter; + let removedFunctionHashes, addedFunctionHashes; + + // reference protocol state + let preUpgradeEntities; + + before(async function () { + // temporary update config, so compiler outputs storage layout + for (const compiler of hre.config.solidity.compilers) { + if (compiler.settings.outputSelection["*"]["BosonVoucher"]) { + compiler.settings.outputSelection["*"]["BosonVoucher"].push("storageLayout"); + } else { + compiler.settings.outputSelection["*"]["BosonVoucher"] = ["storageLayout"]; + } + } + + try { + // Make accounts available + [deployer, rando, clerk, pauser, assistant] = await ethers.getSigners(); + + let contractsBefore; + + ({ + protocolDiamondAddress, + protocolContracts: contractsBefore, + mockContracts, + accessController, + } = await deploySuite(deployer, version)); + + twinHandler = contractsBefore.twinHandler; + delete contractsBefore.twinHandler; + + // Populate protocol with data + preUpgradeEntities = await populateProtocolContract( + deployer, + protocolDiamondAddress, + contractsBefore, + mockContracts, + true + ); + + // Add twin handler back + contractsBefore.twinHandler = twinHandler; + + const preUpgradeStorageLayout = await getStorageLayout("BosonVoucher"); + const preUpgradeEntitiesVoucher = await populateVoucherContract( + deployer, + protocolDiamondAddress, + contractsBefore, + mockContracts, + preUpgradeEntities, + 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 + ); + + const voucherContractState = await getVoucherContractState(preUpgradeEntitiesVoucher); + + ({ bundleHandler, exchangeHandler, twinHandler, disputeHandler } = contractsBefore); + + let getFunctionHashesClosure = getStateModifyingFunctionsHashes( + [ + "SellerHandlerFacet", + "OfferHandlerFacet", + "ConfigHandlerFacet", + "PauseHandlerFacet", + "GroupHandlerFacet", + "OrchestrationHandlerFacet1", + ], + undefined, + [ + "createSeller", + "createOffer", + "createPremintedOffer", + "unpause", + "createGroup", + "setGroupCondition", + "setMaxOffersPerBatch", + "setMaxOffersPerGroup", + "setMaxTwinsPerBundle", + "setMaxOffersPerBundle", + "setMaxTokensPerWithdrawal", + "setMaxFeesPerDisputeResolver", + "setMaxDisputesPerBatch", + "setMaxAllowedSellers", + "setMaxExchangesPerBatch", + "setMaxPremintedVouchers", + ] + ); + + removedFunctionHashes = await getFunctionHashesClosure(); + + // prepare seller creators + const { sellers } = preUpgradeEntities; + + // Start a seller update (finished in tests) + accountHandler = await ethers.getContractAt("IBosonAccountHandler", protocolDiamondAddress); + let { wallet, id, seller, authToken } = sellers[0]; + seller.clerk = rando.address; + await accountHandler.connect(wallet).updateSeller(seller, authToken); + ({ wallet, id, seller, authToken } = sellers[1]); + seller.clerk = rando.address; + seller.assistant = rando.address; + await accountHandler.connect(wallet).updateSeller(seller, authToken); + ({ wallet, id, seller, authToken } = sellers[2]); + seller.clerk = clerk.address; + await accountHandler.connect(wallet).updateSeller(seller, authToken); + await accountHandler.connect(clerk).optInToSellerUpdate(id, [SellerUpdateFields.Clerk]); + const { DRs } = preUpgradeEntities; + let disputeResolver; + ({ wallet, disputeResolver } = DRs[0]); + disputeResolver.clerk = rando.address; + await accountHandler.connect(wallet).updateDisputeResolver(disputeResolver); + ({ wallet, disputeResolver } = DRs[1]); + disputeResolver.clerk = rando.address; + disputeResolver.assistant = rando.address; + await accountHandler.connect(wallet).updateDisputeResolver(disputeResolver); + + shell.exec(`git checkout HEAD scripts`); + await migrate("upgrade-test"); + + // Cast to updated interface + let newHandlers = { + accountHandler: "IBosonAccountHandler", + pauseHandler: "IBosonPauseHandler", + configHandler: "IBosonConfigHandler", + offerHandler: "IBosonOfferHandler", + groupHandler: "IBosonGroupHandler", + orchestrationHandler: "IBosonOrchestrationHandler", + fundsHandler: "IBosonFundsHandler", + exchangeHandler: "IBosonExchangeHandler", + }; + + contractsAfter = { ...contractsBefore }; + + for (const [handlerName, interfaceName] of Object.entries(newHandlers)) { + contractsAfter[handlerName] = await ethers.getContractAt(interfaceName, protocolDiamondAddress); + } + + ({ + accountHandler, + pauseHandler, + configHandler, + offerHandler, + groupHandler, + orchestrationHandler, + fundsHandler, + exchangeHandler, + } = contractsAfter); + + getFunctionHashesClosure = getStateModifyingFunctionsHashes( + [ + "SellerHandlerFacet", + "OfferHandlerFacet", + "ConfigHandlerFacet", + "PauseHandlerFacet", + "GroupHandlerFacet", + "OrchestrationHandlerFacet1", + "ExchangeHandlerFacet", + ], + undefined, + [ + "createSeller", + "createOffer", + "createPremintedOffer", + "unpause", + "createGroup", + "setGroupCondition", + "createNewCollection", + "setMinResolutionPeriod", + "commitToConditionalOffer", + "updateSellerSalt", + ] + ); + + addedFunctionHashes = await getFunctionHashesClosure(); + + snapshot = await getSnapshot(); + + // Get protocol state after the upgrade + protocolContractStateAfter = await getProtocolContractState( + protocolDiamondAddress, + contractsAfter, + mockContracts, + preUpgradeEntities + ); + + const includeTests = [ + "offerContractState", + "bundleContractState", + "disputeContractState", + "fundsContractState", + "twinContractState", + "protocolStatusPrivateContractState", + ]; + + // 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 + ) + ); + + const equalCustomTypes = { + "t_struct(Range)12648_storage": "t_struct(Range)14241_storage", + }; + + context( + "Generic tests on Voucher", + getGenericContextVoucher( + deployer, + protocolDiamondAddress, + contractsAfter, + mockContracts, + voucherContractState, + preUpgradeEntitiesVoucher, + preUpgradeStorageLayout, + snapshot, + equalCustomTypes + ) + ); + } 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 successful + context("šŸ“‹ Breaking changes, new methods and bug fixes", async function () { + context("Breaking changes", async function () { + context("accountContractState", async function () { + it("Existing DR's clerk is changed to 0", async function () { + // Lookup by id + let stateBeforeWithoutClerk = protocolContractStateBefore.accountContractState.DRsState.map((dr) => ({ + ...dr, + DR: { ...dr.DR, clerk: ZeroAddress }, + })); + + // All DR's clerks should be 0 + assert.deepEqual(stateBeforeWithoutClerk, protocolContractStateAfter.accountContractState.DRsState); + + // Lookup by address + stateBeforeWithoutClerk = protocolContractStateBefore.accountContractState.DRbyAddressState.map((dr) => ({ + ...dr, + DR: { ...dr.DR, clerk: ZeroAddress }, + })); + + // All DR's clerks should be 0 + assert.deepEqual(stateBeforeWithoutClerk, protocolContractStateAfter.accountContractState.DRbyAddressState); + }); + + it("Existing seller's clerk is changed to 0", async function () { + // Lookup by id + let stateBeforeWithoutClerk = protocolContractStateBefore.accountContractState.sellerState.map((s) => ({ + ...s, + seller: { ...s.seller, clerk: ZeroAddress }, + })); + + // All Seller's clerks should be 0 + assert.deepEqual(stateBeforeWithoutClerk, protocolContractStateAfter.accountContractState.sellerState); + + // Lookup by address + stateBeforeWithoutClerk = protocolContractStateBefore.accountContractState.sellerByAddressState.map((s) => ({ + ...s, + seller: { ...s.seller, clerk: ZeroAddress }, + })); + + // All Seller's clerks should be 0 + assert.deepEqual( + stateBeforeWithoutClerk, + protocolContractStateAfter.accountContractState.sellerByAddressState + ); + }); + + it("Other account state should not be affected", async function () { + // Agent's and buyer's state should be unaffected + assert.deepEqual( + protocolContractStateBefore.accountContractState.buyersState, + protocolContractStateAfter.accountContractState.buyersState + ); + assert.deepEqual( + protocolContractStateBefore.accountContractState.agentsState, + protocolContractStateAfter.accountContractState.agentsState + ); + }); + }); + + context.skip("Protocol limits", async function () { + let wallets; + let sellers, DRs, sellerWallet; + + before(async function () { + ({ sellers, DRs } = preUpgradeEntities); + ({ wallet: sellerWallet } = sellers[0]); + + wallets = new Array(200); + + for (let i = 0; i < wallets.length; i++) { + wallets[i] = Wallet.createRandom(provider); + } + + await Promise.all( + wallets.map((w) => { + return provider.send("hardhat_setBalance", [w.address, toHexString(parseEther("10000"))]); + }) + ); + + await provider.send("hardhat_setBalance", [sellerWallet.address, toHexString(parseEther("10000"))]); + }); + + it("can complete more exchanges than maxExchangesPerBatch", async function () { + const { maxExchangesPerBatch } = protocolLimits; + const exchangesCount = Number(maxExchangesPerBatch) + 1; + const startingExchangeId = await exchangeHandler.getNextExchangeId(); + const exchangesToComplete = [...Array(exchangesCount).keys()].map((i) => startingExchangeId + BigInt(i)); + + // Create offer with maxExchangesPerBatch+1 items + const { offer, offerDates, offerDurations } = await mockOffer(); + offer.quantityAvailable = exchangesCount; + offer.price = offer.buyerCancelPenalty = offer.sellerDeposit = 0; + const offerId = await offerHandler.getNextOfferId(); + await offerHandler.connect(sellerWallet).createOffer(offer, offerDates, offerDurations, DRs[0].id, "0"); + await setNextBlockTimestamp(Number(offerDates.voucherRedeemableFrom)); + + // Commit to offer and redeem voucher + // Use unique wallets to avoid nonce issues + const walletSet = wallets.slice(0, exchangesCount); + + for (let i = 0; i < exchangesCount; i++) { + const tx = await exchangeHandler.connect(walletSet[i]).commitToOffer(walletSet[i].address, offerId); + const { exchangeId } = getEvent(await tx.wait(), exchangeHandler, "BuyerCommitted"); + await exchangeHandler.connect(walletSet[i]).redeemVoucher(exchangeId); + } + + const { timestamp } = await ethers.provider.getBlock(); + setNextBlockTimestamp(timestamp + Number(offerDurations.disputePeriod) + 1); + + const tx = await exchangeHandler.connect(sellerWallet).completeExchangeBatch(exchangesToComplete); + await expect(tx).to.not.be.reverted; + }); + + it("can create/extend/void more offers than maxOffersPerBatch", async function () { + const { maxOffersPerBatch } = protocolLimits; + const offerCount = Number(maxOffersPerBatch) + 1; + + const { offer, offerDates, offerDurations } = await mockOffer(); + const offers = new Array(offerCount).fill(offer); + const offerDatesList = new Array(offerCount).fill(offerDates); + const offerDurationsList = new Array(offerCount).fill(offerDurations); + const disputeResolverIds = new Array(offerCount).fill(DRs[0].id); + const agentIds = new Array(offerCount).fill("0"); + const startingOfferId = await offerHandler.getNextOfferId(); + + // Create offers in batch + await expect( + offerHandler + .connect(sellerWallet) + .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds) + ).to.not.be.reverted; + + // Extend offers validity + const newValidUntilDate = (BigInt(offerDates.validUntil) + 10000n).toString(); + const offerIds = [...Array(offerCount).keys()].map((i) => startingOfferId + BigInt(i)); + await expect( + offerHandler.connect(sellerWallet).extendOfferBatch(offerIds, newValidUntilDate) + ).to.not.be.reverted; + + // Void offers + await expect(offerHandler.connect(sellerWallet).voidOfferBatch(offerIds)).to.not.be.reverted; + }); + + it("can create a bundle with more twins than maxTwinsPerBundle", async function () { + const { maxTwinsPerBundle } = protocolLimits; + const twinCount = Number(maxTwinsPerBundle) + 1; + const startingTwinId = await twinHandler.getNextTwinId(); + const twinIds = [...Array(twinCount).keys()].map((i) => startingTwinId + BigInt(i)); + + const [twinContract] = await deployMockTokens(["Foreign721"]); + await twinContract.connect(sellerWallet).setApprovalForAll(await twinHandler.getAddress(), true); + + // Create all twins + const twin721 = mockTwin(await twinContract.getAddress(), TokenType.NonFungibleToken); + twin721.amount = "0"; + twin721.supplyAvailable = "1"; + + for (let i = 0; i < twinCount; i++) { + twin721.tokenId = i; + await twinHandler.connect(sellerWallet).createTwin(twin721); + } + + // create an offer with only 1 item, so twins' supply available is enough + const { offer, offerDates, offerDurations } = await mockOffer(); + offer.quantityAvailable = 1; + const offerId = await offerHandler.getNextOfferId(); + await offerHandler.connect(sellerWallet).createOffer(offer, offerDates, offerDurations, DRs[0].id, "0"); + + // Create a bundle with more twins than maxTwinsPerBundle + const bundle = new Bundle("1", sellers[0].seller.id, [offerId], twinIds); + await expect(bundleHandler.connect(sellerWallet).createBundle(bundle)).to.not.be.reverted; + }); + + it("can create a bundle with more offers than maxOffersPerBundle", async function () { + const { maxOffersPerBundle } = protocolLimits; + const offerCount = Number(maxOffersPerBundle) + 1; + const twinId = await twinHandler.getNextTwinId(); + const startingOfferId = await offerHandler.getNextOfferId(); + const offerIds = [...Array(offerCount).keys()].map((i) => startingOfferId + BigInt(i)); + + const { offer, offerDates, offerDurations } = await mockOffer({ refreshModule: true }); + const offers = new Array(offerCount).fill(offer); + const offerDatesList = new Array(offerCount).fill(offerDates); + const offerDurationsList = new Array(offerCount).fill(offerDurations); + const disputeResolverIds = new Array(offerCount).fill(DRs[0].id); + const agentIds = new Array(offerCount).fill("0"); + + // Create offers in batch + await offerHandler + .connect(sellerWallet) + .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds, { + gasLimit: 100000000, // increase gas limit to avoid out of gas error + }); + + // At list one twin is needed to create a bundle + const [twinContract] = await deployMockTokens(["Foreign721"]); + await twinContract.connect(sellerWallet).setApprovalForAll(await twinHandler.getAddress(), true); + const twin721 = mockTwin(await twinContract.getAddress(), TokenType.NonFungibleToken); + twin721.amount = "0"; + await twinHandler.connect(sellerWallet).createTwin(twin721); + + // Create a bundle with more twins than maxTwinsPerBundle + const bundle = new Bundle("1", sellers[0].seller.id, offerIds, [twinId]); + await expect(bundleHandler.connect(sellerWallet).createBundle(bundle)).to.not.be.reverted; + }); + + it("can add more offers to a group than maxOffersPerGroup", async function () { + const { maxOffersPerBundle } = protocolLimits; + const offerCount = Number(maxOffersPerBundle) + 1; + const startingOfferId = await offerHandler.getNextOfferId(); + const offerIds = [...Array(offerCount).keys()].map((i) => startingOfferId + BigInt(i)); + const { offer, offerDates, offerDurations } = await mockOffer(); + const offers = new Array(offerCount).fill(offer); + const offerDatesList = new Array(offerCount).fill(offerDates); + const offerDurationsList = new Array(offerCount).fill(offerDurations); + const disputeResolverIds = new Array(offerCount).fill(DRs[0].id); + const agentIds = new Array(offerCount).fill("0"); + + // Create offers in batch + await offerHandler + .connect(sellerWallet) + .createOfferBatch(offers, offerDatesList, offerDurationsList, disputeResolverIds, agentIds); + + // Create a group with more offers than maxOffersPerGroup + const group = new Group("1", sellers[0].seller.id, offerIds); + const { mockConditionalToken } = mockContracts; + const condition = mockCondition({ + tokenAddress: await mockConditionalToken.getAddress(), + maxCommits: "10", + }); + await expect(groupHandler.connect(sellerWallet).createGroup(group, condition)).to.not.be.reverted; + }); + + it("can withdraw more tokens than maxTokensPerWithdrawal", async function () { + const { maxTokensPerWithdrawal } = protocolLimits; + const tokenCount = Number(maxTokensPerWithdrawal) + 1; + const sellerId = sellers[0].seller.id; + + const tokens = await deployMockTokens(new Array(tokenCount).fill("Foreign20")); + + // Mint tokens and deposit them to the seller + await Promise.all( + wallets.slice(0, tokenCount).map(async (wallet, i) => { + const walletAddress = await wallet.getAddress(); + await provider.send("hardhat_setBalance", [walletAddress, toHexString(parseEther("10"))]); + const token = tokens[i]; + await token.connect(wallet).mint(walletAddress, "1000"); + await token.connect(wallet).approve(await accountHandler.getAddress(), "1000"); + return fundsHandler.connect(wallet).depositFunds(sellerId, await token.getAddress(), "1000"); + }) + ); + + // Withdraw more tokens than maxTokensPerWithdrawal + const tokenAddresses = tokens.map(async (token) => await token.getAddress()); + const amounts = new Array(tokenCount).fill("1000"); + await expect( + fundsHandler.connect(sellerWallet).withdrawFunds(sellerId, tokenAddresses, amounts) + ).to.not.be.reverted; + }); + + it("can create a DR with more fees than maxFeesPerDisputeResolver", async function () { + const { maxFeesPerDisputeResolver } = protocolLimits; + const feeCount = Number(maxFeesPerDisputeResolver) + 1; + + // we just need some address, so we just use "wallets" + const disputeResolverFees = wallets.slice(0, feeCount).map((wallet) => { + return new DisputeResolverFee(wallet.address, "Token", "0"); + }); + const sellerAllowList = []; + const disputeResolver = mockDisputeResolver(rando.address, rando.address, ZeroAddress, rando.address); + + // Create a DR with more fees than maxFeesPerDisputeResolver + await expect( + accountHandler.connect(rando).createDisputeResolver(disputeResolver, disputeResolverFees, sellerAllowList) + ).to.not.be.reverted; + }); + + it("can create a DR with more allowed sellers than maxAllowedSellers", async function () { + const { maxAllowedSellers } = protocolLimits; + const sellerCount = Number(maxAllowedSellers) + 1; + const startingSellerId = await accountHandler.getNextAccountId(); + const sellerAllowList = [...Array(sellerCount).keys()].map((i) => startingSellerId + BigInt(i)); + + // create new sellers + const emptyAuthToken = mockAuthToken(); + const voucherInitValues = mockVoucherInitValues(); + + await Promise.all( + wallets.slice(0, sellerCount).map(async (wallet) => { + const walletAddress = await wallet.getAddress(); + const seller = mockSeller(walletAddress, walletAddress, ZeroAddress, walletAddress, true); + await provider.send("hardhat_setBalance", [walletAddress, toHexString(parseEther("10"))]); + return accountHandler.connect(wallet).createSeller(seller, emptyAuthToken, voucherInitValues); + }) + ); + + const disputeResolverFees = [new DisputeResolverFee(ZeroAddress, "Native", "0")]; + const disputeResolver = mockDisputeResolver(rando.address, rando.address, ZeroAddress, rando.address); + + await expect( + accountHandler.connect(rando).createDisputeResolver(disputeResolver, disputeResolverFees, sellerAllowList) + ).to.not.be.reverted; + }); + + it("can expire more disputes than maxDisputesPerBatch", async function () { + const { maxDisputesPerBatch } = protocolLimits; + const disputesCount = Number(maxDisputesPerBatch) + 1; + const startingExchangeId = await exchangeHandler.getNextExchangeId(); + const disputesToExpire = [...Array(disputesCount).keys()].map((i) => startingExchangeId + BigInt(i)); + // Create offer with maxDisputesPerBatch+1 items + const { offer, offerDates, offerDurations } = await mockOffer(); + offer.quantityAvailable = disputesCount; + offer.price = offer.buyerCancelPenalty = offer.sellerDeposit = 0; + const offerId = await offerHandler.getNextOfferId(); + await offerHandler.connect(sellerWallet).createOffer(offer, offerDates, offerDurations, DRs[0].id, "0"); + + await setNextBlockTimestamp(Number(offerDates.voucherRedeemableFrom)); + // Commit to offer and redeem voucher + // Use unique wallets to avoid nonce issues + const walletSet = wallets.slice(0, disputesCount); + await Promise.all( + walletSet.map(async (wallet) => { + const walletAddress = await wallet.getAddress(); + await provider.send("hardhat_setBalance", [walletAddress, toHexString(parseEther("10"))]); + const tx = await exchangeHandler.connect(wallet).commitToOffer(walletAddress, offerId); + const { exchangeId } = getEvent(await tx.wait(), exchangeHandler, "BuyerCommitted"); + await exchangeHandler.connect(wallet).redeemVoucher(exchangeId); + return disputeHandler.connect(wallet).raiseDispute(exchangeId); + }) + ); + + const { timestamp } = await ethers.provider.getBlock(); + setNextBlockTimestamp(timestamp + Number(offerDurations.resolutionPeriod) + 1); + + // Expire more disputes than maxDisputesPerBatch + await expect(disputeHandler.connect(sellerWallet).expireDisputeBatch(disputesToExpire)).to.not.be.reverted; + }); + + it("can premint more vouchers than maxPremintedVouchers", async function () { + const { maxPremintedVouchers } = protocolLimits; + const voucherCount = Number(maxPremintedVouchers) + 1; + const offerId = await offerHandler.getNextOfferId(); + + // Create offer with maxPremintedVouchers+1 items + const { offer, offerDates, offerDurations } = await mockOffer(); + offer.quantityAvailable = voucherCount; + await offerHandler.connect(sellerWallet).createOffer(offer, offerDates, offerDurations, DRs[0].id, "0"); + + // reserve range + await offerHandler.connect(sellerWallet).reserveRange(offerId, voucherCount, sellerWallet.address); + + // Premint more vouchers than maxPremintedVouchers + const { voucherContractAddress } = sellers[0]; + + const bosonVoucher = await getContractAt("BosonVoucher", voucherContractAddress); + const tx = await bosonVoucher.connect(sellerWallet).preMint(offerId, voucherCount); + + await expect(tx).to.not.be.reverted; + }); + }); + + context("SellerHandler", async function () { + let seller, emptyAuthToken, voucherInitValues; + + beforeEach(async function () { + // Fix account id. nextAccountId is 16 and current is 15 + accountId.next(); + seller = mockSeller(assistant.address, assistant.address, assistant.address, assistant.address, true); + emptyAuthToken = mockAuthToken(); + voucherInitValues = mockVoucherInitValues(); + }); + + context("Deprecate clerk", async function () { + it("Cannot create a new seller with non zero clerk", async function () { + // Attempt to create a seller with clerk not 0 + await expect( + accountHandler.connect(assistant).createSeller(seller, emptyAuthToken, voucherInitValues) + ).to.revertedWith(RevertReasons.CLERK_DEPRECATED); + }); + + it("Cannot update a seller to non zero clerk", async function () { + seller.clerk = ZeroAddress; + await accountHandler.connect(assistant).createSeller(seller, emptyAuthToken, voucherInitValues); + + // Attempt to update a seller, expecting revert + seller.clerk = assistant.address; + await expect(accountHandler.connect(assistant).updateSeller(seller, emptyAuthToken)).to.revertedWith( + RevertReasons.CLERK_DEPRECATED + ); + }); + + it("Cannot opt-in to non zero clerk [no other pending update]", async function () { + const { sellers } = preUpgradeEntities; + const { id } = sellers[0]; + + // Attempt to update a seller, expecting revert + await expect( + accountHandler.connect(rando).optInToSellerUpdate(id, [SellerUpdateFields.Clerk]) + ).to.revertedWith(RevertReasons.NO_PENDING_UPDATE_FOR_ACCOUNT); + }); + + it("Cannot opt-in to non zero clerk [other pending updates]", async function () { + const { sellers } = preUpgradeEntities; + const { id } = sellers[1]; + + // Attempt to update a seller, expecting revert + await expect( + accountHandler.connect(rando).optInToSellerUpdate(id, [SellerUpdateFields.Clerk]) + ).to.revertedWith(RevertReasons.CLERK_DEPRECATED); + }); + + it("It's possible to create a new account that uses the same address as some old clerk address", async function () { + // "clerk" was used as a clerk address for seller[2] before the upgrade + seller = mockSeller(clerk.address, clerk.address, ZeroAddress, clerk.address); + await expect(accountHandler.connect(clerk).createSeller(seller, emptyAuthToken, voucherInitValues)).to.emit( + accountHandler, + "SellerCreated" + ); + }); + + const { sellers } = preUpgradeEntities; + const { wallet, id } = sellers[2]; // seller 2 assistant was different from clerk + + // Withdraw funds + await expect(fundsHandler.connect(wallet).withdrawFunds(id, [], [])).to.emit(fundsHandler, "FundsWithdrawn"); + }); + + context("Clear pending updates", async function () { + let pendingSellerUpdate, authToken; + beforeEach(async function () { + authToken = new AuthToken("8400", AuthTokenType.Lens); + + pendingSellerUpdate = seller.clone(); + pendingSellerUpdate.admin = ZeroAddress; + pendingSellerUpdate.clerk = ZeroAddress; + pendingSellerUpdate.assistant = ZeroAddress; + pendingSellerUpdate.treasury = ZeroAddress; + pendingSellerUpdate.active = false; + pendingSellerUpdate.id = "0"; + }); + + it("should clean pending addresses update when calling updateSeller again", async function () { + // create a seller with auth token + seller.admin = ZeroAddress; + seller.clerk = ZeroAddress; + const nextAccountId = await accountHandler.getNextAccountId(); + seller.id = nextAccountId.toString(); + + await mockContracts.mockAuthERC721Contract.connect(assistant).mint(8400, 1); + authToken = new AuthToken("8400", AuthTokenType.Lens); + + await accountHandler.connect(assistant).createSeller(seller, authToken, voucherInitValues); + + // Start replacing auth token with admin address, but don't complete it + seller.admin = pendingSellerUpdate.admin = assistant.address; + + await expect(accountHandler.connect(assistant).updateSeller(seller, emptyAuthToken)) + .to.emit(accountHandler, "SellerUpdatePending") + .withArgs(seller.id, pendingSellerUpdate.toStruct(), emptyAuthToken.toStruct(), assistant.address); + + // Replace admin address with auth token + seller.admin = pendingSellerUpdate.admin = ZeroAddress; + + await mockContracts.mockAuthERC721Contract.connect(assistant).mint(123, 1); + authToken.tokenId = "123"; + + // Calling updateSeller again, request to replace admin with an auth token + await expect(accountHandler.connect(assistant).updateSeller(seller, authToken)) + .to.emit(accountHandler, "SellerUpdatePending") + .withArgs(seller.id, pendingSellerUpdate.toStruct(), authToken.toStruct(), assistant.address); + }); + + it("should clean pending auth token update when calling updateSeller again", async function () { + seller.clerk = ZeroAddress; + const nextAccountId = await accountHandler.getNextAccountId(); + seller.id = nextAccountId.toString(); + + await accountHandler.connect(assistant).createSeller(seller, emptyAuthToken, voucherInitValues); + + // Start replacing admin address with auth token, but don't complete it + seller.admin = pendingSellerUpdate.admin = ZeroAddress; + authToken = new AuthToken("8400", AuthTokenType.Lens); + + await mockContracts.mockAuthERC721Contract.connect(assistant).mint(8400, 1); + + await expect(accountHandler.connect(assistant).updateSeller(seller, authToken)) + .to.emit(accountHandler, "SellerUpdatePending") + .withArgs(seller.id, pendingSellerUpdate.toStruct(), authToken.toStruct(), assistant.address); + + // Replace auth token with admin address + seller.admin = pendingSellerUpdate.admin = rando.address; + + // Calling updateSeller for the second time, request to replace auth token with admin + await expect(accountHandler.connect(assistant).updateSeller(seller, emptyAuthToken)) + .to.emit(accountHandler, "SellerUpdatePending") + .withArgs(seller.id, pendingSellerUpdate.toStruct(), emptyAuthToken.toStruct(), assistant.address); + }); + }); + + context("Create new collection", async function () { + let beaconProxyAddress; + before(async function () { + // Get the beacon proxy address + beaconProxyAddress = await calculateBosonProxyAddress(protocolDiamondAddress); + }); + + it("New seller can create a new collection", async function () { + const seller = mockSeller(assistant.address, assistant.address, ZeroAddress, assistant.address); + seller.id = await accountHandler.getNextAccountId(); + const emptyAuthToken = mockAuthToken(); + const voucherInitValues = mockVoucherInitValues(); + + await accountHandler.connect(assistant).createSeller(seller, emptyAuthToken, voucherInitValues); + + const externalId = "new-collection"; + voucherInitValues.collectionSalt = encodeBytes32String(externalId); + + const expectedDefaultAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + seller.admin + ); // default + const expectedCollectionAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + seller.admin, + voucherInitValues.collectionSalt + ); + const tx = await accountHandler.connect(assistant).createNewCollection(externalId, voucherInitValues); + + await expect(tx) + .to.emit(accountHandler, "CollectionCreated") + .withArgs(Number(seller.id), 1, expectedCollectionAddress, externalId, assistant.address); + + const expectedCollections = new CollectionList([new Collection(expectedCollectionAddress, externalId)]); + + // Get the collections information + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(seller.id); + const additionalCollections = CollectionList.fromStruct(collections); + + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + + // Voucher clone contract + let bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + expect(await bosonVoucher.owner()).to.equal(assistant.address, "Wrong voucher clone owner"); + + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + + expect(await bosonVoucher.contractURI()).to.equal(voucherInitValues.contractURI, "Wrong contract URI"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " S" + seller.id + "_C1", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_S" + seller.id + "_C1", + "Wrong voucher client symbol" + ); + }); + + it("old seller can create a new collection", async function () { + const { sellers } = preUpgradeEntities; + const { + wallet: sellerWallet, + id: sellerId, + voucherInitValues, + seller, + voucherContractAddress: expectedDefaultAddress, + } = sellers[0]; + const externalId = "new-collection"; + voucherInitValues.collectionSalt = encodeBytes32String(externalId); + beaconProxyAddress = await calculateBosonProxyAddress(protocolDiamondAddress); + const expectedCollectionAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + seller.admin, + voucherInitValues.collectionSalt, + voucherInitValues.collectionSalt + ); + + await expect(accountHandler.connect(sellerWallet).createNewCollection(externalId, voucherInitValues)) + .to.emit(accountHandler, "CollectionCreated") + .withArgs(sellerId, 1, expectedCollectionAddress, externalId, sellerWallet.address); + + const expectedCollections = new CollectionList([new Collection(expectedCollectionAddress, externalId)]); + + // Get the collections information + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollections(sellerId); + + const additionalCollections = CollectionList.fromStruct(collections); + + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(additionalCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + + // Voucher clone contract + let bosonVoucher = await ethers.getContractAt("OwnableUpgradeable", expectedCollectionAddress); + + expect(await bosonVoucher.owner()).to.equal(sellerWallet.address, "Wrong voucher clone owner"); + + bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + expect(await bosonVoucher.contractURI()).to.equal(voucherInitValues.contractURI, "Wrong contract URI"); + expect(await bosonVoucher.name()).to.equal( + VOUCHER_NAME + " S" + sellerId + "_C1", + "Wrong voucher client name" + ); + expect(await bosonVoucher.symbol()).to.equal( + VOUCHER_SYMBOL + "_S" + sellerId + "_C1", + "Wrong voucher client symbol" + ); + }); + }); + + it("New sellers uses create2 to calculate voucher address", async function () { + const seller = mockSeller(assistant.address, assistant.address, ZeroAddress, assistant.address); + seller.id = await accountHandler.getNextAccountId(); + + const emptyAuthToken = mockAuthToken(); + const voucherInitValues = mockVoucherInitValues(); + + const beaconProxyAddress = await calculateBosonProxyAddress(protocolDiamondAddress); + const defaultVoucherAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + seller.admin, + voucherInitValues.collectionSalt, + voucherInitValues.collectionSalt + ); + + const tx = await accountHandler.connect(assistant).createSeller(seller, emptyAuthToken, voucherInitValues); + await expect(tx) + .to.emit(accountHandler, "SellerCreated") + .withArgs( + seller.id, + seller.toStruct(), + defaultVoucherAddress, + emptyAuthToken.toStruct(), + assistant.address + ); + }); + }); + + context("DisputeResolverHandler", async function () { + let disputeResolver, disputeResolverFees, sellerAllowList; + + beforeEach(async function () { + disputeResolver = mockDisputeResolver(rando.address, rando.address, rando.address, rando.address); + disputeResolverFees = []; + sellerAllowList = []; + }); + + it("Cannot create a new DR with non zero clerk", async function () { + // Attempt to create a DR with clerk not 0 + await expect( + accountHandler.connect(rando).createDisputeResolver(disputeResolver, disputeResolverFees, sellerAllowList) + ).to.revertedWith(RevertReasons.CLERK_DEPRECATED); + }); + + it("Cannot update a DR to non zero clerk", async function () { + disputeResolver.clerk = ZeroAddress; + await accountHandler + .connect(rando) + .createDisputeResolver(disputeResolver, disputeResolverFees, sellerAllowList); + + // Attempt to update a DR, expecting revert + disputeResolver.clerk = rando.address; + await expect(accountHandler.connect(rando).updateDisputeResolver(disputeResolver)).to.revertedWith( + RevertReasons.CLERK_DEPRECATED + ); + }); + + it("Cannot opt-in to non zero clerk [no other pending update]", async function () { + const { DRs } = preUpgradeEntities; + const { id } = DRs[0]; + + // Attempt to update a DR, expecting revert + await expect( + accountHandler.connect(rando).optInToDisputeResolverUpdate(id, [DisputeResolverUpdateFields.Clerk]) + ).to.revertedWith(RevertReasons.NO_PENDING_UPDATE_FOR_ACCOUNT); + }); + + it("Cannot opt-in to non zero clerk [other pending updates]", async function () { + const { DRs } = preUpgradeEntities; + const { id } = DRs[1]; + + // Attempt to update a DR, expecting revert + await expect( + accountHandler.connect(rando).optInToDisputeResolverUpdate(id, [DisputeResolverUpdateFields.Clerk]) + ).to.revertedWith(RevertReasons.CLERK_DEPRECATED); + }); + + it("It's possible to create a new account that uses the same address as some old clerk address", async function () { + // "clerk" was used as a clerk address for DR[2] before the upgrade + disputeResolver = mockDisputeResolver(clerk.address, clerk.address, ZeroAddress, clerk.address); + await expect( + accountHandler.connect(clerk).createDisputeResolver(disputeResolver, disputeResolverFees, sellerAllowList) + ).to.emit(accountHandler, "DisputeResolverCreated"); + }); + }); + + context("PauseHandler", async function () { + it("should emit a ProtocolUnpaused event", async function () { + // Grant PAUSER role to pauser account + await accessController.grantRole(Role.PAUSER, pauser.address); + + const regions = [PausableRegion.Sellers, PausableRegion.DisputeResolvers]; + + // Pause protocol + await pauseHandler.connect(pauser).pause(regions); + + // Unpause the protocol, testing for the event + await expect(pauseHandler.connect(pauser).unpause(regions)) + .to.emit(pauseHandler, "ProtocolUnpaused") + .withArgs(regions, pauser.address); + }); + + it("getPausedRegions function should be available", async function () { + const pausedRegions = await pauseHandler.getPausedRegions(); + expect(pausedRegions).to.not.reverted; + }); + }); + + context("ConfigHandler", async function () { + it("After the upgrade, minimal resolution period is set", async function () { + // Minimal resolution period should be set to 1 week + const minResolutionPeriod = await configHandler.connect(rando).getMinResolutionPeriod(); + expect(minResolutionPeriod).to.equal(oneWeek); + }); + + it("It is possible to change minimal resolution period", async function () { + const minResolutionPeriod = BigInt(oneMonth); + // Set new resolution period + await expect(configHandler.connect(deployer).setMinResolutionPeriod(minResolutionPeriod)) + .to.emit(configHandler, "MinResolutionPeriodChanged") + .withArgs(minResolutionPeriod, deployer.address); + + const tx = await configHandler.connect(rando).getMinResolutionPeriod(); + // Verify that new value is stored + await expect(tx).to.equal(minResolutionPeriod); + }); + + it("State of configContractState is not affected apart from minResolutionPeriod, removed limits and beaconProxy", async function () { + // make a shallow copy to not modify original protocolContractState as it's used on getGenericContext + const configContractStateBefore = { ...protocolContractStateBefore.configContractState }; + const configContractStateAfter = { ...protocolContractStateAfter.configContractState }; + + const { minResolutionPeriod: minResolutionPeriodAfter, beaconProxyAddress } = configContractStateAfter; + + configContractStateBefore.maxOffersPerBatch = "0"; + configContractStateBefore.maxOffersPerGroup = "0"; + configContractStateBefore.maxOffersPerBundle = "0"; + configContractStateBefore.maxTwinsPerBundle = "0"; + configContractStateBefore.maxTokensPerWithdrawal = "0"; + configContractStateBefore.maxFeesPerDisputeResolver = "0"; + configContractStateBefore.maxEscalationResponsePeriod = "0"; + configContractStateBefore.maxDisputesPerBatch = "0"; + configContractStateBefore.maxAllowedSellers = "0"; + configContractStateBefore.maxExchangesPerBatch = "0"; + configContractStateBefore.maxPremintedVouchers = "0"; + + delete configContractStateBefore.minResolutionPeriod; + delete configContractStateAfter.minResolutionPeriod; + delete configContractStateBefore.beaconProxyAddress; + delete configContractStateAfter.beaconProxyAddress; + + const expectedBeaconProxyAddress = await calculateBosonProxyAddress(protocolDiamondAddress); + expect(minResolutionPeriodAfter).to.equal(oneWeek); + expect(beaconProxyAddress).to.equal(expectedBeaconProxyAddress); + + expect(configContractStateAfter).to.deep.equal(configContractStateBefore); + }); + }); + + context("OfferHandler", async function () { + it("Cannot make an offer with too short resolution period", async function () { + const { sellers, DRs } = preUpgradeEntities; + const { wallet } = sellers[0]; + + // Set dispute duration period to 0 + const { offer, offerDates, offerDurations } = await mockOffer(); + offerDurations.resolutionPeriod = (BigInt(oneWeek) - 10n).toString(); + + // Attempt to Create an offer, expecting revert + await expect( + offerHandler.connect(wallet).createOffer(offer, offerDates, offerDurations, DRs[0].id, "0") + ).to.revertedWith(RevertReasons.INVALID_RESOLUTION_PERIOD); + }); + + it("Create an offer with a new collection", async function () { + const { sellers, DRs, buyers } = preUpgradeEntities; + const { wallet: sellerWallet, voucherInitValues, seller } = sellers[0]; + const { disputeResolver } = DRs[0]; + const { wallet: buyerWallet } = buyers[0]; + const externalId = "new-collection"; + voucherInitValues.collectionSalt = encodeBytes32String(externalId); + + // Get next ids + const offerId = await offerHandler.getNextOfferId(); + const exchangeId = await exchangeHandler.getNextExchangeId(); + const tokenId = deriveTokenId(offerId, exchangeId); + + // Create a new collection + await accountHandler.connect(sellerWallet).createNewCollection(externalId, voucherInitValues); + + const { offer, offerDates, offerDurations } = await mockOffer(); + offer.collectionIndex = "1"; + + await expect( + offerHandler.connect(sellerWallet).createOffer(offer, offerDates, offerDurations, disputeResolver.id, "0") + ).to.emit(offerHandler, "OfferCreated"); + + // Deposit seller funds so the commit will succeed + const sellerPool = BigInt(offer.quantityAvailable) * BigInt(offer.price); + await fundsHandler + .connect(sellerWallet) + .depositFunds(seller.id, offer.exchangeToken, sellerPool, { value: sellerPool }); + + const beaconProxyAddress = await calculateBosonProxyAddress(protocolDiamondAddress); + + // Collection voucher contract + const expectedCollectionAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + seller.admin, + voucherInitValues.collectionSalt, + voucherInitValues.collectionSalt + ); + + const bosonVoucher = await ethers.getContractAt("IBosonVoucher", expectedCollectionAddress); + + const tx = await exchangeHandler + .connect(buyerWallet) + .commitToOffer(buyerWallet.address, offerId, { value: offer.price }); + + // Voucher should be minted on a new collection contract + await expect(tx).to.emit(bosonVoucher, "Transfer").withArgs(ZeroAddress, buyerWallet.address, tokenId); + }); + }); + + context("ExchangeHandler", async function () { + context("Twin transfers", async function () { + let mockTwin721Contract, twin721; + let buyer; + let exchangeId; + let sellerWallet, sellerId; + + beforeEach(async function () { + const { buyers, sellers, DRs } = preUpgradeEntities; + ({ wallet: sellerWallet, id: sellerId } = sellers[0]); + ({ wallet: buyer } = buyers[0]); + // find a DR with no allowlist + const { disputeResolver } = DRs.find((DR) => DR.sellerAllowList.length == 0); + const { mockToken, mockTwinTokens } = mockContracts; + [mockTwin721Contract] = mockTwinTokens; + + // Create twin + const twinId = await twinHandler.getNextTwinId(); + twin721 = mockTwin(await mockTwin721Contract.getAddress(), TokenType.NonFungibleToken); + twin721.supplyAvailable = "1"; + twin721.sellerId = sellerId; + twin721.amount = "0"; + twin721.tokenId = "1"; + // mint last token as will be the one used for the offer + await mockTwin721Contract.connect(sellerWallet).mint(twin721.supplyAvailable, 1); + await mockTwin721Contract.connect(sellerWallet).setApprovalForAll(protocolDiamondAddress, true); + await twinHandler.connect(sellerWallet).createTwin(twin721); + + // Create offer + const { offer, offerDates, offerDurations } = await mockOffer(); + const offerId = await offerHandler.getNextOfferId(); + + await expect( + offerHandler.connect(sellerWallet).createOffer(offer, offerDates, offerDurations, disputeResolver.id, "0") + ).to.emit(offerHandler, "OfferCreated"); + + // Deposit seller funds so the commit will succeed + const sellerPool = BigInt(offer.quantityAvailable) * BigInt(offer.price); + await fundsHandler + .connect(sellerWallet) + .depositFunds(sellerId, offer.exchangeToken, sellerPool, { value: sellerPool }); + + // Bundle: Required constructor params + const bundleId = await bundleHandler.getNextBundleId(); + const offerIds = [offerId]; // createBundle() does not accept empty offer ids. + const twinIds = [twinId]; + + // Create a new bundle + let bundle = new Bundle(bundleId, sellerId, offerIds, twinIds); + await bundleHandler.connect(sellerWallet).createBundle(bundle); + + exchangeId = await exchangeHandler.getNextExchangeId(); + let msgValue; + if (offer.exchangeToken == ZeroAddress) { + msgValue = offer.price; + } else { + // approve token transfer + msgValue = 0; + await mockToken.connect(buyer).approve(protocolDiamondAddress, offer.price); + await mockToken.mint(buyer.address, offer.price); + } + // Commit to offer + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offerId, { value: msgValue }); + + await setNextBlockTimestamp(Number(offerDates.voucherRedeemableFrom)); + }); + + it("If a twin is transferred it could be used in a new twin", async function () { + const tx = await exchangeHandler.connect(buyer).redeemVoucher(exchangeId, { gasLimit: 10000000 }); + const receipt = await tx.wait(); + const event = getEvent(receipt, exchangeHandler, "TwinTransferred"); + + const { tokenId } = event; + + // transfer the twin to the original seller + await mockTwin721Contract.connect(buyer).safeTransferFrom(buyer.address, sellerWallet.address, tokenId); + + // create a new twin with the transferred token + twin721.id = tokenId; + twin721.supplyAvailable = 1; + twin721.amount = "0"; + await expect(twinHandler.connect(sellerWallet).createTwin(twin721)).to.emit(twinHandler, "TwinCreated"); + }); + + it("if twin transfer fail, dispute is raised even when buyer is EOA", async function () { + // Remove the approval for the protocol to transfer the seller's tokens + await mockTwin721Contract.connect(sellerWallet).setApprovalForAll(protocolDiamondAddress, false); + + const tx = await exchangeHandler.connect(buyer).redeemVoucher(exchangeId, { gasLimit: 10000000 }); + + await expect(tx) + .to.emit(exchangeHandler, "TwinTransferFailed") + .withArgs(twin721.id, twin721.tokenAddress, exchangeId, anyValue, twin721.amount, buyer.address); + + // Get the exchange state + const [, response] = await exchangeHandler.connect(rando).getExchangeState(exchangeId); + + // It should match ExchangeState.Disputed + assert.equal(response, ExchangeState.Disputed, "Exchange state is incorrect"); + }); + + it("if twin transfers consume all available gas, redeem still succeeds, but dispute is raised", async function () { + const { sellers, offers, buyers } = preUpgradeEntities; + const { wallet, id: sellerId, offerIds } = sellers[1]; // first seller has condition to all offers + const offerId = offerIds[offerIds.length - 1]; + const { + offer: { price, quantityAvailable, exchangeToken }, + } = offers[offerId - 1]; + const { wallet: buyer, id: buyerId } = buyers[0]; + + const [foreign20gt, foreign20gt_2] = await deployMockTokens(["Foreign20GasTheft", "Foreign20GasTheft"]); + + // Approve the protocol diamond to transfer seller's tokens + await foreign20gt.connect(wallet).approve(protocolDiamondAddress, "100"); + await foreign20gt_2.connect(wallet).approve(protocolDiamondAddress, "100"); + + // Create two ERC20 twins that will consume all available gas + const twin20 = mockTwin(await foreign20gt.getAddress()); + twin20.amount = "1"; + twin20.supplyAvailable = quantityAvailable; + twin20.id = Number(await twinHandler.getNextTwinId()); + + await twinHandler.connect(wallet).createTwin(twin20.toStruct()); + + const twin20_2 = twin20.clone(); + twin20_2.id = twin20.id + 1; + twin20_2.tokenAddress = await foreign20gt_2.getAddress(); + await twinHandler.connect(wallet).createTwin(twin20_2.toStruct()); + + // Create a new bundle + const bundle = new Bundle("2", sellerId, [offerId], [twin20.id, twin20_2.id]); + await bundleHandler.connect(wallet).createBundle(bundle.toStruct()); + + let msgValue; + if (exchangeToken == ZeroAddress) { + msgValue = price; + } else { + // approve token transfer + msgValue = 0; + const { mockToken } = mockContracts; + await mockToken.mint(buyer.address, price); + await mockToken.connect(buyer).approve(protocolDiamondAddress, price); + } + + // Commit to offer + const exchangeId = await exchangeHandler.getNextExchangeId(); + await exchangeHandler.connect(buyer).commitToOffer(buyer.address, offerId, { value: msgValue }); + + // Redeem the voucher + const tx = await exchangeHandler.connect(buyer).redeemVoucher(exchangeId, { gasLimit: 1000000 }); // limit gas to speed up test + + // Dispute should be raised and both transfers should fail + await expect(tx) + .to.emit(disputeHandler, "DisputeRaised") + .withArgs(exchangeId, buyerId, sellerId, buyer.address); + + await expect(tx) + .to.emit(exchangeHandler, "TwinTransferFailed") + .withArgs(twin20.id, twin20.tokenAddress, exchangeId, twin20.tokenId, twin20.amount, buyer.address); + + await expect(tx).to.emit(exchangeHandler, "TwinTransferFailed").withArgs( + twin20_2.id, + twin20_2.tokenAddress, + exchangeId, + + twin20_2.tokenId, + twin20_2.amount, + buyer.address + ); + + // Get the exchange state + const [, response] = await exchangeHandler.connect(rando).getExchangeState(exchangeId); + + // It should match ExchangeState.Revoked + assert.equal(response, ExchangeState.Disputed, "Exchange state is incorrect"); + }); + }); + + it("commit exactly at offer expiration timestamp", async function () { + const { offers, buyers } = preUpgradeEntities; + const { offer, offerDates } = offers[1]; //offer 0 has a condition + const { wallet: buyer } = buyers[0]; + const { mockToken } = mockContracts; + + await mockToken.mint(buyer.address, offer.price); + + // allow the protocol to transfer the buyer's tokens + await mockToken.connect(buyer).approve(protocolDiamondAddress, offer.price); + + await setNextBlockTimestamp(Number(offerDates.validUntil)); + + // Commit to offer, retrieving the event + await expect(exchangeHandler.connect(buyer).commitToOffer(buyer.address, offer.id)).to.emit( + exchangeHandler, + "BuyerCommitted" + ); + }); + + it("old gated offers work ok with new token gating", async function () { + const { groups, buyers, offers } = preUpgradeEntities; + const { wallet: buyer } = buyers[0]; + const { offerIds } = groups[0]; + const { offer } = offers[offerIds[0] - 1]; + + const tx = await exchangeHandler + .connect(buyer) + .commitToConditionalOffer(buyer.address, offer.id, "0", { value: offer.price }); + + // Commit to offer, retrieving the event + await expect(tx).to.emit(exchangeHandler, "BuyerCommitted"); + }); + }); + + context("GroupHandler", async function () { + it("it's possible to create a group with new token gating", async function () { + const [conditionToken1155] = await deployMockTokens(["Foreign1155"]); + // create a condition that was not possible before + const condition = mockCondition( + { + tokenType: TokenType.MultiToken, + tokenAddress: await conditionToken1155.getAddress(), + maxTokenId: "15", + minTokenId: "5", + method: EvaluationMethod.Threshold, + threshold: "2", + gating: GatingType.PerAddress, + }, + { refreshModule: true } + ); + + const seller = preUpgradeEntities.sellers[1]; // seller does not have any group + const group = new Group(1, seller.seller.id, seller.offerIds); // group all seller's offers + + await expect(groupHandler.connect(seller.wallet).createGroup(group, condition)).to.emit( + groupHandler, + "GroupCreated" + ); + }); + }); + + context("FundsHandler", async function () { + it("new methods to get funds work", async function () { + // just check the consistency of the return values + const { sellers } = preUpgradeEntities; + const { mockToken } = mockContracts; + + const expectedTokenListSet = new Set([await mockToken.getAddress(), ZeroAddress]); + for (const seller of sellers) { + const { id } = seller; + + const tokenList = await fundsHandler.getTokenList(id); + const tokenListSet = new Set(tokenList); + + if (seller.id == 1) { + // first seller has only 1 offer with native token + expect(tokenListSet).to.deep.equal(new Set([ZeroAddress])); + } else { + expect(tokenListSet).to.deep.equal(expectedTokenListSet); + } + + const tokenListPaginated = await fundsHandler.getTokenListPaginated(id, tokenList.length, "0"); + + expect(tokenListPaginated).to.deep.equal(tokenList); + + const allAvailableFunds = FundsList.fromStruct(await fundsHandler.getAllAvailableFunds(id)); + const tokenListFromAvailableFunds = allAvailableFunds.funds.map((f) => f.tokenAddress); + expect(tokenListFromAvailableFunds).to.deep.equal(tokenList); + + const av = await fundsHandler.getAvailableFunds(id, [...tokenList]); + const availableFunds = FundsList.fromStruct(av); + expect(availableFunds).to.deep.equal(allAvailableFunds); + } + }); + }); + + context("OrchestrationHandler", async function () { + // NB: testing only 1 method to confirm that orchestration is upgraded + // The rest of the method are tested in the unit tests + it("should emit a SellerCreated and OfferCreated events with empty auth token", async function () { + const { DRs } = preUpgradeEntities; + + const { disputeResolver } = DRs.find((DR) => DR.sellerAllowList.length == 0); + const seller = mockSeller(assistant.address, assistant.address, ZeroAddress, assistant.address); + const emptyAuthToken = mockAuthToken(); + const voucherInitValues = mockVoucherInitValues(); + const { offer, offerDates, offerDurations } = await mockOffer(); + + // Create a seller and an offer, testing for the event + const tx = await orchestrationHandler + .connect(assistant) + .createSellerAndOffer( + seller, + offer, + offerDates, + offerDurations, + disputeResolver.id, + emptyAuthToken, + voucherInitValues, + "0" + ); + + await expect(tx).to.emit(orchestrationHandler, "SellerCreated"); + await expect(tx).to.emit(orchestrationHandler, "OfferCreated"); + + // Get the beacon proxy address + const beaconProxyAddress = await calculateBosonProxyAddress(await configHandler.getAddress()); + + // expected address of the first clone + const expectedCloneAddress = calculateCloneAddress( + await orchestrationHandler.getAddress(), + beaconProxyAddress, + assistant.address + ); + + let bosonVoucher = await getContractAt("IBosonVoucher", expectedCloneAddress); + + await expect(tx).to.emit(bosonVoucher, "ContractURIChanged"); + await expect(tx).to.emit(bosonVoucher, "RoyaltyPercentageChanged"); + bosonVoucher = await getContractAt("OwnableUpgradeable", expectedCloneAddress); + await expect(tx).to.emit(bosonVoucher, "OwnershipTransferred"); + }); + }); + + context("MetaTransactionHandler", async function () { + it("Function hashes from removedFunctionsHashes list should not be allowlisted", async function () { + for (const hash of removedFunctionHashes) { + // get function name from hash + const isFunctionAllowlisted = contractsAfter.metaTransactionsHandler.getFunction( + "isFunctionAllowlisted(bytes32)" + ); + const isAllowed = await isFunctionAllowlisted.staticCall(hash); + expect(isAllowed).to.be.false; + } + }); + + it("Function hashes from from addedFunctionsHashes list should be allowlisted", async function () { + for (const hash of addedFunctionHashes) { + const isFunctionAllowlisted = await contractsAfter.metaTransactionsHandler.getFunction( + "isFunctionAllowlisted(bytes32)" + ); + + const isAllowed = await isFunctionAllowlisted.staticCall(hash); + expect(isAllowed).to.be.true; + } + }); + + it("State of metaTxPrivateContractState is not affected apart from 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 + ); + }); + }); + + context("BosonVoucher", async function () { + let bosonVoucher; + + beforeEach(async function () { + const seller = mockSeller(assistant.address, assistant.address, ZeroAddress, assistant.address); + seller.id = await accountHandler.getNextAccountId(); + const emptyAuthToken = mockAuthToken(); + const voucherInitValues = mockVoucherInitValues(); + + await accountHandler.connect(assistant).createSeller(seller, emptyAuthToken, voucherInitValues); + + const beaconProxyAddress = await calculateBosonProxyAddress(protocolDiamondAddress); + + const expectedDefaultAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + seller.admin + ); // default + bosonVoucher = await getContractAt("BosonVoucher", expectedDefaultAddress); + }); + + it("callExternalContract returns whatever External contract returned", async function () { + // Deploy a random contract + const MockSimpleContract = await getContractFactory("MockSimpleContract"); + const mockSimpleContract = await MockSimpleContract.deploy(); + await mockSimpleContract.waitForDeployment(); + + const calldata = mockSimpleContract.interface.encodeFunctionData("testReturn"); + const returnedValueRaw = await bosonVoucher + .connect(assistant) + .callExternalContract.staticCall(await mockSimpleContract.getAddress(), calldata); + const abiCoder = new ethers.AbiCoder(); + const [returnedValue] = abiCoder.decode(["string"], returnedValueRaw); + expect(returnedValue).to.equal("TestValue"); + }); + + it("tokenURI function should revert if tokenId does not exist", async function () { + await expect(bosonVoucher.tokenURI(666)).to.be.revertedWith(RevertReasons.ERC721_INVALID_TOKEN_ID); + }); + }); + }); + }); +}); diff --git a/test/upgrade/clients/01_generic.js b/test/upgrade/clients/01_generic.js index 874738474..6c10a4016 100644 --- a/test/upgrade/clients/01_generic.js +++ b/test/upgrade/clients/01_generic.js @@ -17,7 +17,8 @@ function getGenericContext( voucherContractState, preUpgradeEntities, preUpgradeStorageLayout, - snapshot + snapshot, + equalCustomTypes ) { const genericContextFunction = async function () { afterEach(async function () { @@ -43,7 +44,7 @@ function getGenericContext( const postUpgradeStorageLayout = await getStorageLayout("BosonVoucher"); assert( - compareStorageLayouts(preUpgradeStorageLayout, postUpgradeStorageLayout), + compareStorageLayouts(preUpgradeStorageLayout, postUpgradeStorageLayout, equalCustomTypes), "Upgrade breaks storage layout" ); }); @@ -71,7 +72,7 @@ function getGenericContext( // Get protocol state after the upgrade. Get the data that should be in location of old data. const voucherContractStateAfterUpgradeAndActions = await getVoucherContractState(preUpgradeEntities); - // The only thing that should change are buyers's balances, since they comitted to new offers and they got vouchers for them. + // The only thing that should change are buyers's balances, since they committed to new offers and they got vouchers for them. // Modify the post upgrade state to reflect the expected changes const { buyers, sellers } = preUpgradeEntities; const entities = [...sellers, ...buyers]; @@ -90,7 +91,7 @@ function getGenericContext( const buyerIndex = entities.findIndex((e) => e.wallet == buyerWallet); // Update the balance of the buyer - voucherData.balanceOf[buyerIndex] = voucherData.balanceOf[buyerIndex] - 1; + voucherData.balanceOf[buyerIndex] = voucherData.balanceOf[buyerIndex] - 1n; } } diff --git a/test/upgrade/clients/BosonVoucher-2.1.0-2.2.0.js b/test/upgrade/clients/BosonVoucher-2.1.0-2.2.0.js index ab3a2f670..4fda393ac 100644 --- a/test/upgrade/clients/BosonVoucher-2.1.0-2.2.0.js +++ b/test/upgrade/clients/BosonVoucher-2.1.0-2.2.0.js @@ -11,6 +11,7 @@ const { getVoucherContractState, revertState, } = require("../../util/upgrade"); + const { mockDisputeResolver, mockSeller, @@ -88,7 +89,7 @@ describe("[@skip-on-coverage] After client upgrade, everything is still operatio protocolContracts, mockContracts, undefined, // no existing entities - oldVersion + true ); voucherContractState = await getVoucherContractState(preUpgradeEntities); @@ -340,7 +341,7 @@ describe("[@skip-on-coverage] After client upgrade, everything is still operatio // Deploy a random contract const MockSimpleContract = await getContractFactory("MockSimpleContract"); const mockSimpleContract = await MockSimpleContract.deploy(); - await mockSimpleContract.deployed(); + await mockSimpleContract.waitForDeployment(); // Generate calldata const calldata = mockSimpleContract.interface.encodeFunctionData("testEvent"); diff --git a/test/util/mock.js b/test/util/mock.js index 050bcdf54..8e1dfe3ac 100644 --- a/test/util/mock.js +++ b/test/util/mock.js @@ -2,10 +2,10 @@ const hre = require("hardhat"); const { ZeroAddress, provider, parseUnits } = hre.ethers; const decache = require("decache"); -const Condition = require("../../scripts/domain/Condition"); +let Condition = require("../../scripts/domain/Condition.js"); const EvaluationMethod = require("../../scripts/domain/EvaluationMethod"); +let Offer = require("../../scripts/domain/Offer"); const GatingType = require("../../scripts/domain/GatingType"); -const Offer = require("../../scripts/domain/Offer"); const OfferDates = require("../../scripts/domain/OfferDates"); const OfferFees = require("../../scripts/domain/OfferFees"); const OfferDurations = require("../../scripts/domain/OfferDurations"); @@ -65,7 +65,12 @@ async function mockOfferDates() { } // Returns a mock offer with price in native token -async function mockOffer() { +async function mockOffer({ refreshModule } = {}) { + if (refreshModule) { + decache("../../scripts/domain/Offer.js"); + Offer = require("../../scripts/domain/Offer.js"); + } + const id = "1"; const sellerId = "1"; // argument sent to contract for createOffer will be ignored const price = parseUnits("1.5", "ether").toString(); @@ -113,12 +118,18 @@ function mockTwin(tokenAddress, tokenType) { return new Twin(id, sellerId, amount, supplyAvailable, tokenId, tokenAddress, tokenType); } -function mockDisputeResolver(assistantAddress, adminAddress, clerkAddress, treasuryAddress, active, refreshModule) { +function mockDisputeResolver( + assistantAddress, + adminAddress, + clerkAddress = ZeroAddress, + treasuryAddress, + active, + refreshModule +) { if (refreshModule) { decache("../../scripts/domain/DisputeResolver.js"); DisputeResolver = require("../../scripts/domain/DisputeResolver.js"); } - const metadataUriDR = `https://ipfs.io/ipfs/disputeResolver1`; return new DisputeResolver( accountId.next().value, @@ -251,16 +262,27 @@ async function mockReceipt() { ); } -function mockCondition({ - method, - tokenType, - tokenAddress, - gating, - minTokenId, - threshold, - maxCommits, - maxTokenId, -} = {}) { +function mockCondition( + { method, tokenType, tokenAddress, gating, minTokenId, threshold, maxCommits, maxTokenId } = {}, + { refreshModule, legacyCondition } = {} +) { + if (refreshModule) { + decache("../../scripts/domain/Condition.js"); + Condition = require("../../scripts/domain/Condition.js"); + } + + if (legacyCondition) { + const tokenId = minTokenId; + return new Condition( + method ?? EvaluationMethod.Threshold, + tokenType ?? TokenType.FungibleToken, + tokenAddress ?? ZeroAddress, + tokenId ?? "0", + threshold ?? "1", + maxCommits ?? "1" + ); + } + return new Condition( method ?? EvaluationMethod.Threshold, tokenType ?? TokenType.FungibleToken, diff --git a/test/util/upgrade.js b/test/util/upgrade.js index 2968de301..84eba16a8 100644 --- a/test/util/upgrade.js +++ b/test/util/upgrade.js @@ -2,9 +2,11 @@ const shell = require("shelljs"); const _ = require("lodash"); const { getStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); const hre = require("hardhat"); +const decache = require("decache"); const { + id: ethersId, keccak256, - formatBytes32String, + encodeBytes32String, ZeroAddress, getContractAt, Wallet, @@ -13,6 +15,7 @@ const { toUtf8Bytes, getContractFactory, getSigners, + ZeroHash, } = hre.ethers; const AuthToken = require("../../scripts/domain/AuthToken"); const { getMetaTransactionsHandlerFacetInitArgs } = require("../../scripts/config/facet-deploy.js"); @@ -34,19 +37,14 @@ const { mockCondition, mockTwin, } = require("./mock"); -const { - setNextBlockTimestamp, - paddingType, - getMappingStoragePosition, - calculateContractAddress, -} = require("./utils.js"); +const { setNextBlockTimestamp, paddingType, getMappingStoragePosition } = require("./utils.js"); const { oneMonth, oneDay } = require("./constants"); const { getInterfaceIds } = require("../../scripts/config/supported-interfaces.js"); const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens"); const { readContracts } = require("../../scripts/util/utils"); const { getFacets } = require("../upgrade/00_config"); const Receipt = require("../../scripts/domain/Receipt"); -const Offer = require("../../scripts/domain/Offer"); +let Offer = require("../../scripts/domain/Offer"); const OfferFees = require("../../scripts/domain/OfferFees"); const DisputeResolutionTerms = require("../../scripts/domain/DisputeResolutionTerms"); const OfferDurations = require("../../scripts/domain/OfferDurations"); @@ -56,10 +54,11 @@ const DisputeResolver = require("../../scripts/domain/DisputeResolver"); const Agent = require("../../scripts/domain/Agent"); const Buyer = require("../../scripts/domain/Buyer"); const { tagsByVersion } = require("../upgrade/00_config"); +let Condition = require("../../scripts/domain/Condition"); // Common vars const versionsWithActivateDRFunction = ["v2.0.0", "v2.1.0"]; -const versionsWithClerkRole = ["v2.0.0", "v2.1.0", "v2.2.0", "v2.2.1"]; +const versionsBelowV2_3 = ["v2.0.0", "v2.1.0", "v2.2.0", "v2.2.1"]; // have clerk role, don't have collections, different way to get available funds let rando; let preUpgradeInterfaceIds, preUpgradeVersions; let facets, versionTags; @@ -76,7 +75,7 @@ function getVersionsBeforeTarget(versions, targetVersion) { return versionsBefore.map((version) => { // Remove "v" prefix and "-rc.${number}" suffix - return formatBytes32String(version.replace(/^v/, "").replace(/-rc\.\d+$/, "")); + return encodeBytes32String(version.replace(/^v/, "").replace(/-rc\.\d+$/, "")); }); } @@ -87,13 +86,13 @@ async function deploySuite(deployer, newVersion) { facets = await getFacets(); // checkout old version - const { oldVersion: tag, deployScript: scriptsTag } = versionTags; + const { oldVersion: tag, deployScript: scriptsTag, updateDomain } = versionTags; console.log(`Fetching tags`); shell.exec(`git fetch --force --tags origin`); console.log(`Checking out version ${tag}`); shell.exec(`rm -rf contracts/*`); - shell.exec(`find contracts -type f | xargs git checkout ${tag} --`); + shell.exec(`git checkout ${tag} contracts/**`); if (scriptsTag) { console.log(`Checking out scripts on version ${scriptsTag}`); @@ -101,8 +100,15 @@ async function deploySuite(deployer, newVersion) { shell.exec(`git checkout ${scriptsTag} scripts/**`); } + if (updateDomain) { + console.log(`Updating the domain definitions to ${tag}`); + const filesToUpdate = updateDomain.map((file) => `scripts/domain/${file}.js`).join(" "); + shell.exec(`git checkout ${tag} ${filesToUpdate}`); + } + const isOldOZVersion = ["v2.0", "v2.1", "v2.2"].some((v) => tag.startsWith(v)); if (isOldOZVersion) { + console.log("Installing correct version of OZ"); // Temporary install old OZ contracts shell.exec("npm i @openzeppelin/contracts-upgradeable@4.7.1"); } @@ -166,9 +172,13 @@ async function deploySuite(deployer, newVersion) { await deployMockTokens(["Foreign20", "Foreign20", "Foreign721", "Foreign721", "Foreign20", "Foreign1155"]); const mockTwinTokens = [mockTwin721_1, mockTwin721_2]; - if (isOldOZVersion) { - shell.exec(`git checkout ${tag} package.json package-lock.json`); - shell.exec("npm i"); + // After v2.3.0, the deploy suite does deploy beacon proxy anymore, since it's deployed by the protocol itself + // To make upgrade tests consistent, we deploy it here + if (versionsBelowV2_3.includes(tag)) { + const ClientProxy = await getContractFactory("BeaconClientProxy"); + const bosonVoucherProxy = await ClientProxy.deploy(); + + await configHandler.setBeaconProxyAddress(await bosonVoucherProxy.getAddress()); } return { @@ -197,6 +207,7 @@ async function deploySuite(deployer, newVersion) { mockTwin20, mockTwin1155, }, + accessController, }; } @@ -206,7 +217,7 @@ async function upgradeSuite(protocolDiamondAddress, upgradedInterfaces, override if (!versionTags) { throw new Error("Version tags not cached"); } - const { newVersion: tag, upgradeScript: scriptsTag } = versionTags; + const { newVersion: tag, upgradeScript: scriptsTag, updateDomain } = versionTags; shell.exec(`rm -rf contracts/*`); shell.exec(`rm -rf scripts/*`); @@ -229,6 +240,12 @@ async function upgradeSuite(protocolDiamondAddress, upgradedInterfaces, override shell.exec(`git checkout HEAD contracts`); } + if (updateDomain) { + console.log(`Updating the domain definitions to ${tag || "HEAD"}`); + const filesToUpdate = updateDomain.map((file) => `scripts/domain/${file}.js`).join(" "); + shell.exec(`git checkout ${tag || "HEAD"} ${filesToUpdate}`); + } + if (!facets) facets = await getFacets(); let facetConfig = facets.upgrade[tag] || facets.upgrade["latest"]; @@ -255,11 +272,11 @@ async function upgradeSuite(protocolDiamondAddress, upgradedInterfaces, override // upgrade the clients to new version async function upgradeClients() { // Upgrade Clients - shell.exec(`rm -rf contracts/*`); shell.exec(`git checkout HEAD scripts`); const tag = versionTags.newVersion; // checkout the new tag + shell.exec(`rm -rf contracts/*`); console.log(`Checking out version ${tag}`); shell.exec(`git checkout ${tag} contracts`); @@ -321,6 +338,7 @@ async function populateProtocolContract( let twins = []; let exchanges = []; let bundles = []; + let bosonVouchers = []; const entityType = { SELLER: 0, @@ -348,8 +366,6 @@ async function populateProtocolContract( ]; let nextAccountId = Number(await accountHandler.getNextAccountId()); - let voucherIndex = 1; - for (const entity of entities) { const wallet = Wallet.createRandom(); const connectedWallet = wallet.connect(provider); @@ -360,13 +376,13 @@ async function populateProtocolContract( value: parseEther("10"), }; await deployer.sendTransaction(tx); - // create entities switch (entity) { case entityType.DR: { - const clerkAddress = versionsWithClerkRole.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion) - ? await wallet.getAddress() + const clerkAddress = versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion) + ? wallet.address : ZeroAddress; + const disputeResolver = mockDisputeResolver( await wallet.getAddress(), await wallet.getAddress(), @@ -375,6 +391,7 @@ async function populateProtocolContract( true, true ); + const disputeResolverFees = [ new DisputeResolverFee(ZeroAddress, "Native", "0"), new DisputeResolverFee(await mockToken.getAddress(), "MockToken", "0"), @@ -397,20 +414,15 @@ async function populateProtocolContract( //ADMIN role activates Dispute Resolver await accountHandler.connect(deployer).activateDisputeResolver(disputeResolver.id); } + break; } case entityType.SELLER: { - const clerkAddress = versionsWithClerkRole.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion) - ? await wallet.getAddress() + const clerkAddress = versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion) + ? wallet.address : ZeroAddress; - const seller = mockSeller( - await wallet.getAddress(), - await wallet.getAddress(), - clerkAddress, - await wallet.getAddress(), - true - ); + const seller = mockSeller(wallet.address, wallet.address, clerkAddress, wallet.address, true); const id = (seller.id = nextAccountId.toString()); let authToken; @@ -425,11 +437,16 @@ async function populateProtocolContract( await mockAuthERC721Contract.connect(connectedWallet).mint(101 * id, 1); authToken = new AuthToken(`${101 * id}`, AuthTokenType.Lens); } + // set unique new voucherInitValues - const voucherInitValues = new VoucherInitValues(`http://seller${id}.com/uri`, id * 10); - await accountHandler.connect(connectedWallet).createSeller(seller, authToken, voucherInitValues); + const voucherInitValues = versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion) + ? new VoucherInitValues(`http://seller${id}.com/uri`, id * 10) + : new VoucherInitValues(`http://seller${id}.com/uri`, id * 10, ZeroHash); + const tx = await accountHandler.connect(connectedWallet).createSeller(seller, authToken, voucherInitValues); + + const receipt = await tx.wait(); + const [, , voucherContractAddress] = receipt.logs.find((e) => e?.fragment?.name === "SellerCreated").args; - const voucherContractAddress = calculateContractAddress(await accountHandler.getAddress(), voucherIndex++); // ToDo: make version based calculation sellers.push({ wallet: connectedWallet, id, @@ -440,9 +457,13 @@ async function populateProtocolContract( voucherContractAddress, }); + const bosonVoucher = await getContractAt("BosonVoucher", voucherContractAddress); + bosonVouchers.push(bosonVoucher); + // mint mock token to sellers just in case they need them await mockToken.mint(await connectedWallet.getAddress(), "10000000000"); await mockToken.connect(connectedWallet).approve(protocolDiamondAddress, "10000000000"); + break; } case entityType.AGENT: { @@ -462,6 +483,7 @@ async function populateProtocolContract( // mint them conditional token in case they need it await mockConditionalToken.mint(await wallet.getAddress(), "10"); + break; } } @@ -498,14 +520,14 @@ async function populateProtocolContract( } // Set unique offer dates based on offer id - const now = offerDates.validFrom; - offerDates.validFrom = (BigInt(now) + oneMonth + BigInt(offerId) * 1000n).toString(); - offerDates.validUntil = (BigInt(now) + oneMonth * 6n * BigInt(offerId) + 1n).toString(); + const now = BigInt(offerDates.validFrom); + offerDates.validFrom = (now + oneMonth + BigInt(offerId) * 1000n).toString(); + offerDates.validUntil = (now + oneMonth * 6n * BigInt(offerId) + 1n).toString(); // Set unique offerDurations based on offer id - offerDurations.disputePeriod = `${(offerId + 1) * oneMonth}`; - offerDurations.voucherValid = `${(offerId + 1) * oneMonth}`; - offerDurations.resolutionPeriod = `${(offerId + 1) * oneDay}`; + offerDurations.disputePeriod = `${(offerId + 1) * Number(oneMonth)}`; + offerDurations.voucherValid = `${(offerId + 1) * Number(oneMonth)}`; + offerDurations.resolutionPeriod = `${(offerId + 1) * Number(oneDay)}`; // choose one DR and agent const disputeResolverId = DRs[offerId % 3].disputeResolver.id; @@ -534,96 +556,119 @@ async function populateProtocolContract( let groupId = Number(await groupHandler.getNextGroupId()); for (let i = 0; i < sellers.length; i = i + 2) { const seller = sellers[i]; - const group = new Group(groupId, seller.seller.id, seller.offerIds); // group all seller's offers - const condition = mockCondition({ - tokenAddress: await mockConditionalToken.getAddress(), - maxCommits: "10", - }); + const { offerIds } = seller; + const group = new Group(groupId, seller.seller.id, offerIds); // group all seller's offers + const condition = mockCondition( + { + tokenAddress: await mockConditionalToken.getAddress(), + maxCommits: "10", + }, + { + refreshModule: true, + legacyCondition: versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion), + } + ); await groupHandler.connect(seller.wallet).createGroup(group, condition); groups.push(group); + for (const offerId of offerIds) { + const offer = offers.find((o) => o.offer.id == offerId); + offer.groupId = groupId; + } groupId++; } - // create some twins and bundles - let twinId = Number(await twinHandler.getNextTwinId()); - let bundleId = Number(await bundleHandler.getNextBundleId()); - for (let i = 1; i < sellers.length; i = i + 2) { - const seller = sellers[i]; - const sellerId = seller.id; - let twinIds = []; // used for bundle + if (twinHandler) { + // create some twins and bundles + let twinId = Number(await twinHandler.getNextTwinId()); + let bundleId = Number(await bundleHandler.getNextBundleId()); + for (let i = 1; i < sellers.length; i = i + 2) { + const seller = sellers[i]; + const sellerId = seller.id; + let twinIds = []; // used for bundle + + // non fungible token + await mockTwinTokens[0].connect(seller.wallet).setApprovalForAll(protocolDiamondAddress, true); + await mockTwinTokens[1].connect(seller.wallet).setApprovalForAll(protocolDiamondAddress, true); + + // create multiple ranges + const twin721 = mockTwin(ZeroAddress, TokenType.NonFungibleToken); + twin721.amount = "0"; + + // min supply available for twin721 is the total amount to cover all offers bundled + const minSupplyAvailable = offers + .map((o) => o.offer) + .filter((o) => seller.offerIds.includes(Number(o.id))) + .reduce((acc, o) => acc + Number(o.quantityAvailable), 0); + + for (let j = 0; j < 3; j++) { + twin721.tokenId = `${sellerId * 1000000 + j * 100000}`; + twin721.supplyAvailable = minSupplyAvailable; + twin721.tokenAddress = await mockTwinTokens[j % 2].getAddress(); // oscilate between twins + twin721.id = twinId; + + // mint tokens to be transferred on redeem + // ToDo: for the future, change this to shorten the test + let tokensToMint = BigInt(minSupplyAvailable); + let tokenIdToMint = BigInt(twin721.tokenId); + while (tokensToMint > 500n) { + await mockTwinTokens[j % 2].connect(seller.wallet).mint(tokenIdToMint, 500n); + tokensToMint -= 500n; + tokenIdToMint += 500n; + } - // non fungible token - await mockTwinTokens[0].connect(seller.wallet).setApprovalForAll(protocolDiamondAddress, true); - await mockTwinTokens[1].connect(seller.wallet).setApprovalForAll(protocolDiamondAddress, true); + await mockTwinTokens[j % 2].connect(seller.wallet).mint(tokenIdToMint, tokensToMint); + await twinHandler.connect(seller.wallet).createTwin(twin721); - // create multiple ranges - const twin721 = mockTwin(ZeroAddress, TokenType.NonFungibleToken); - twin721.amount = "0"; + twins.push(twin721); + twinIds.push(twinId); - // min supply available for twin721 is the total amount to cover all offers bundled - const minSupplyAvailable = offers - .map((o) => o.offer) - .filter((o) => seller.offerIds.includes(Number(o.id))) - .reduce((acc, o) => acc + Number(o.quantityAvailable), 0); + twinId++; + } + + // fungible + const twin20 = mockTwin(await mockTwin20.getAddress(), TokenType.FungibleToken); + twin20.id = twinId; + twin20.amount = sellerId; + twin20.supplyAvailable = twin20.amount * 100000000; - for (let j = 0; j < 7; j++) { - twin721.tokenId = `${sellerId * 1000000 + j * 100000}`; - twin721.supplyAvailable = minSupplyAvailable; - twin721.tokenAddress = mockTwinTokens[j % 2].address; // oscilate between twins - twin721.id = twinId; + await mockTwin20.connect(seller.wallet).approve(protocolDiamondAddress, twin20.supplyAvailable); // mint tokens to be transferred on redeem - await mockTwinTokens[j % 2].connect(seller.wallet).mint(twin721.tokenId, twin721.supplyAvailable); - await twinHandler.connect(seller.wallet).createTwin(twin721); + await mockTwin20.connect(seller.wallet).mint(seller.wallet, twin20.supplyAvailable * twin20.amount); + await twinHandler.connect(seller.wallet).createTwin(twin20); - twins.push(twin721); + twins.push(twin20); twinIds.push(twinId); - twinId++; - } - - // fungible - const twin20 = mockTwin(await mockTwin20.getAddress(), TokenType.FungibleToken); - - twin20.id = twinId; - twin20.amount = sellerId; - twin20.supplyAvailable = twin20.amount * 100000000; - await mockTwin20.connect(seller.wallet).approve(protocolDiamondAddress, twin20.supplyAvailable); - - // mint tokens to be transferred on redeem - await mockTwin20.connect(seller.wallet).mint(seller.wallet, twin20.supplyAvailable * twin20.amount); - await twinHandler.connect(seller.wallet).createTwin(twin20); + // multitoken twin + const twin1155 = mockTwin(await mockTwin1155.getAddress(), TokenType.MultiToken); + twin1155.id = twinId; - twins.push(twin20); - twinIds.push(twinId); - twinId++; + await mockTwin1155.connect(seller.wallet).setApprovalForAll(protocolDiamondAddress, true); + for (let j = 0; j < 3; j++) { + twin1155.tokenId = `${j * 30000 + sellerId * 300}`; + twin1155.amount = sellerId + j; + twin1155.supplyAvailable = `${300000 * (sellerId + 1)}`; + twin1155.id = twinId; - // multitoken twin - const twin1155 = mockTwin(await mockTwin1155.getAddress(), TokenType.MultiToken); - await mockTwin1155.connect(seller.wallet).setApprovalForAll(protocolDiamondAddress, true); - for (let j = 0; j < 3; j++) { - twin1155.tokenId = `${j * 30000 + sellerId * 300}`; - twin1155.amount = sellerId + j; - twin1155.supplyAvailable = `${300000 * (sellerId + 1)}`; - twin1155.id = twinId; + // mint tokens to be transferred on redeem + await mockTwin1155.connect(seller.wallet).mint(twin1155.tokenId, twin1155.supplyAvailable); + await twinHandler.connect(seller.wallet).createTwin(twin1155); - // mint tokens to be transferred on redeem - await mockTwin1155.connect(seller.wallet).mint(twin1155.tokenId, twin1155.supplyAvailable); - await twinHandler.connect(seller.wallet).createTwin(twin1155); + twins.push(twin1155); + twinIds.push(twinId); + twinId++; + } - twins.push(twin1155); - twinIds.push(twinId); - twinId++; + // create bundle with all seller's twins and offers + const bundle = new Bundle(bundleId, seller.seller.id, seller.offerIds, twinIds); + await bundleHandler.connect(seller.wallet).createBundle(bundle); + bundles.push(bundle); + bundleId++; } - - // create bundle with all seller's twins and offers - const bundle = new Bundle(bundleId, seller.seller.id, seller.offerIds, twinIds); - await bundleHandler.connect(seller.wallet).createBundle(bundle); - bundles.push(bundle); - bundleId++; } // commit to some offers: first buyer commit to 1 offer, second to 2, third to 3 etc @@ -631,9 +676,10 @@ async function populateProtocolContract( let exchangeId = Number(await exchangeHandler.getNextExchangeId()); for (let i = 0; i < buyers.length; i++) { for (let j = i; j < buyers.length; j++) { - const offer = offers[i + j].offer; // some offers will be picked multiple times, some never. + const { offer, groupId } = offers[i + j]; // some offers will be picked multiple times, some never. const offerPrice = offer.price; const buyerWallet = buyers[j].wallet; + let msgValue; if (offer.exchangeToken == ZeroAddress) { msgValue = offerPrice; @@ -643,9 +689,27 @@ async function populateProtocolContract( await mockToken.connect(buyerWallet).approve(protocolDiamondAddress, offerPrice); await mockToken.mint(await buyerWallet.getAddress(), offerPrice); } - await exchangeHandler - .connect(buyerWallet) - .commitToOffer(await buyerWallet.getAddress(), offer.id, { value: msgValue }); + // v2.3.0 introduces commitToConditionalOffer method which should be used for conditional offers + const isAfterV2_3_0 = !versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion); + if (groupId && isAfterV2_3_0) { + // get condition + let [, , condition] = await groupHandler.getGroup(groupId); + decache("../../scripts/domain/Condition.js"); + Condition = require("../../scripts/domain/Condition.js"); + condition = Condition.fromStruct(condition); + + // commit to conditional offer + await exchangeHandler + .connect(buyerWallet) + .commitToConditionalOffer(await buyerWallet.getAddress(), offer.id, condition.minTokenId, { + value: msgValue, + }); + } else { + await exchangeHandler + .connect(buyerWallet) + .commitToOffer(await buyerWallet.getAddress(), offer.id, { value: msgValue }); + } + exchanges.push({ exchangeId: exchangeId, offerId: offer.id, buyerIndex: j }); exchangeId++; } @@ -654,7 +718,9 @@ async function populateProtocolContract( // redeem some vouchers #4 for (const id of [2, 5, 11, 8]) { const exchange = exchanges[id - 1]; - await exchangeHandler.connect(buyers[exchange.buyerIndex].wallet).redeemVoucher(exchange.exchangeId); + await exchangeHandler + .connect(buyers[exchange.buyerIndex].wallet) + .redeemVoucher(exchange.exchangeId, { gasLimit: 10000000 }); } // cancel some vouchers #3 @@ -674,9 +740,13 @@ async function populateProtocolContract( // raise dispute on some exchanges #1 const id = 5; // must be one of redeemed ones const exchange = exchanges[id - 1]; + const offer = offers.find((o) => o.offer.id == exchange.offerId); + const seller = sellers.find((s) => s.seller.id == offer.offer.sellerId); + await disputeHandler.connect(buyers[exchange.buyerIndex].wallet).raiseDispute(exchange.exchangeId); + await disputeHandler.connect(seller.wallet).extendDisputeTimeout(exchange.exchangeId, 4000000000n); - return { DRs, sellers, buyers, agents, offers, exchanges, bundles, groups, twins }; + return { DRs, sellers, buyers, agents, offers, exchanges, bundles, groups, twins, bosonVouchers }; } // Returns protocol state for provided entities @@ -694,7 +764,8 @@ async function getProtocolContractState( configHandler, }, { mockToken, mockTwinTokens }, - { DRs, sellers, buyers, agents, offers, exchanges, bundles, groups, twins } + { DRs, sellers, buyers, agents, offers, exchanges, bundles, groups, twins }, + isBefore = false ) { rando = (await getSigners())[10]; // random account making the calls @@ -713,13 +784,13 @@ async function getProtocolContractState( protocolStatusPrivateContractState, protocolLookupsPrivateContractState, ] = await Promise.all([ - getAccountContractState(accountHandler, { DRs, sellers, buyers, agents }), + getAccountContractState(accountHandler, { DRs, sellers, buyers, agents }, isBefore), getOfferContractState(offerHandler, offers), getExchangeContractState(exchangeHandler, exchanges), getBundleContractState(bundleHandler, bundles), - getConfigContractState(configHandler), + getConfigContractState(configHandler, isBefore), getDisputeContractState(disputeHandler, exchanges), - getFundsContractState(fundsHandler, { DRs, sellers, buyers, agents }), + getFundsContractState(fundsHandler, { DRs, sellers, buyers, agents }, isBefore), getGroupContractState(groupHandler, groups), getTwinContractState(twinHandler, twins), getMetaTxContractState(), @@ -728,7 +799,7 @@ async function getProtocolContractState( getProtocolLookupsPrivateContractState( protocolDiamondAddress, { mockToken, mockTwinTokens }, - { sellers, DRs, agents, buyers, offers, groups } + { sellers, DRs, agents, buyers, offers, groups, twins } ), ]); @@ -749,7 +820,7 @@ async function getProtocolContractState( }; } -async function getAccountContractState(accountHandler, { DRs, sellers, buyers, agents }) { +async function getAccountContractState(accountHandler, { DRs, sellers, buyers, agents }, isBefore = false) { const accountHandlerRando = accountHandler.connect(rando); // all accounts const accounts = [...sellers, ...DRs, ...buyers, ...agents]; @@ -762,20 +833,23 @@ async function getAccountContractState(accountHandler, { DRs, sellers, buyers, a let sellerByAuthTokenState = []; let DRbyAddressState = []; let nextAccountId; + let sellersCollections = []; // Query even the ids where it's not expected to get the entity for (const account of accounts) { const id = account.id; DRsState.push(await getDisputeResolver(accountHandlerRando, id, { getBy: "id" })); - try { - sellerState.push(await getSeller(accountHandlerRando, id, { getBy: "id" })); - } catch (e) { - console.log(e); - } + sellerState.push(await getSeller(accountHandlerRando, id, { getBy: "id" })); agentsState.push(await getAgent(accountHandlerRando, id)); buyersState.push(await getBuyer(accountHandlerRando, id)); + if (!versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion)) { + sellersCollections.push(await accountHandlerRando.getSellersCollections(id)); + } else { + sellersCollections.push([ZeroAddress, []]); + } + for (const account2 of accounts) { const id2 = account2.id; allowedSellersState.push(await accountHandlerRando.areSellersAllowed(id2, [id])); @@ -808,8 +882,10 @@ async function getAccountContractState(accountHandler, { DRs, sellers, buyers, a buyersState, sellerByAddressState, sellerByAuthTokenState, + agentsState, DRbyAddressState, nextAccountId, + sellersCollections, }; } @@ -828,6 +904,9 @@ async function getOfferContractState(offerHandler, offers) { ]); let [exist, offerStruct, offerDates, offerDurations, disputeResolutionTerms, offerFees] = singleOffersState; + decache("../../scripts/domain/Offer.js"); + Offer = require("../../scripts/domain/Offer.js"); + offerStruct = Offer.fromStruct(offerStruct); offerDates = OfferDates.fromStruct(offerDates); offerDurations = OfferDurations.fromStruct(offerDurations); @@ -901,7 +980,8 @@ async function getBundleContractState(bundleHandler, bundles) { return { bundlesState, bundleIdByOfferState, bundleIdByTwinState, nextBundleId }; } -async function getConfigContractState(configHandler) { +async function getConfigContractState(configHandler, isBefore = false) { + const isBefore2_3_0 = versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion); const configHandlerRando = configHandler.connect(rando); const [ tokenAddress, @@ -930,6 +1010,8 @@ async function getConfigContractState(configHandler) { maxResolutionPeriod, minDisputePeriod, accessControllerAddress, + maxPremintedVouchers, + minResolutionPeriod, ] = await Promise.all([ configHandlerRando.getTokenAddress(), configHandlerRando.getTreasuryAddress(), @@ -937,26 +1019,28 @@ async function getConfigContractState(configHandler) { configHandlerRando.getBeaconProxyAddress(), configHandlerRando.getProtocolFeePercentage(), configHandlerRando.getProtocolFeeFlatBoson(), - configHandlerRando.getMaxOffersPerBatch(), - configHandlerRando.getMaxOffersPerGroup(), - configHandlerRando.getMaxTwinsPerBundle(), - configHandlerRando.getMaxOffersPerBundle(), - configHandlerRando.getMaxTokensPerWithdrawal(), - configHandlerRando.getMaxFeesPerDisputeResolver(), - configHandlerRando.getMaxEscalationResponsePeriod(), - configHandlerRando.getMaxDisputesPerBatch(), + isBefore2_3_0 ? configHandlerRando.getMaxOffersPerBatch() : Promise.resolve(0n), + isBefore2_3_0 ? configHandlerRando.getMaxOffersPerGroup() : Promise.resolve(0n), + isBefore2_3_0 ? configHandlerRando.getMaxTwinsPerBundle() : Promise.resolve(0n), + isBefore2_3_0 ? configHandlerRando.getMaxOffersPerBundle() : Promise.resolve(0n), + isBefore2_3_0 ? configHandlerRando.getMaxTokensPerWithdrawal() : Promise.resolve(0n), + isBefore2_3_0 ? configHandlerRando.getMaxFeesPerDisputeResolver() : Promise.resolve(0n), + isBefore2_3_0 ? configHandlerRando.getMaxEscalationResponsePeriod() : Promise.resolve(0n), + isBefore2_3_0 ? configHandlerRando.getMaxDisputesPerBatch() : Promise.resolve(0n), configHandlerRando.getMaxTotalOfferFeePercentage(), - configHandlerRando.getMaxAllowedSellers(), + isBefore2_3_0 ? configHandlerRando.getMaxAllowedSellers() : Promise.resolve(0n), configHandlerRando.getBuyerEscalationDepositPercentage(), configHandlerRando.getAuthTokenContract(AuthTokenType.None), configHandlerRando.getAuthTokenContract(AuthTokenType.Custom), configHandlerRando.getAuthTokenContract(AuthTokenType.Lens), configHandlerRando.getAuthTokenContract(AuthTokenType.ENS), - configHandlerRando.getMaxExchangesPerBatch(), + isBefore2_3_0 ? configHandlerRando.getMaxExchangesPerBatch() : Promise.resolve(0n), configHandlerRando.getMaxRoyaltyPecentage(), configHandlerRando.getMaxResolutionPeriod(), configHandlerRando.getMinDisputePeriod(), configHandlerRando.getAccessControllerAddress(), + isBefore2_3_0 ? configHandlerRando.getMaxPremintedVouchers() : Promise.resolve(0n), + !isBefore2_3_0 ? configHandlerRando.getMinResolutionPeriod() : Promise.resolve(0n), ]); return { @@ -986,6 +1070,8 @@ async function getConfigContractState(configHandler) { maxResolutionPeriod: maxResolutionPeriod.toString(), minDisputePeriod: minDisputePeriod.toString(), accessControllerAddress, + maxPremintedVouchers: maxPremintedVouchers.toString(), + minResolutionPeriod: minResolutionPeriod.toString(), }; } @@ -1014,14 +1100,18 @@ async function getDisputeContractState(disputeHandler, exchanges) { return { disputesState, disputesStatesState, disputeTimeoutState, isDisputeFinalizedState }; } -async function getFundsContractState(fundsHandler, { DRs, sellers, buyers, agents }) { +async function getFundsContractState(fundsHandler, { DRs, sellers, buyers, agents }, isBefore = false) { const fundsHandlerRando = fundsHandler.connect(rando); // Query even the ids where it's not expected to get the entity const accountIds = [...DRs, ...sellers, ...buyers, ...agents].map((account) => account.id); - const groupsState = await Promise.all(accountIds.map((id) => fundsHandlerRando.getAllAvailableFunds(id))); - - return { groupsState }; + let fundsState = []; + if (versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion)) { + fundsState = await Promise.all(accountIds.map((id) => fundsHandlerRando.getAvailableFunds(id))); + } else { + fundsState = await Promise.all(accountIds.map((id) => fundsHandlerRando.getAllAvailableFunds(id))); + } + return { fundsState }; } async function getGroupContractState(groupHandler, groups) { @@ -1109,7 +1199,7 @@ async function getMetaTxPrivateContractState(protocolDiamondAddress) { // get also hashFunction hashInfoState.push({ typeHash: await getStorageAt(protocolDiamondAddress, storageSlot), - functionPointer: await getStorageAt(protocolDiamondAddress, BigInt(storageSlot) + 1), + functionPointer: await getStorageAt(protocolDiamondAddress, BigInt(storageSlot) + 1n), }); } const isAllowlistedState = {}; @@ -1193,45 +1283,47 @@ async function getProtocolStatusPrivateContractState(protocolDiamondAddress) { async function getProtocolLookupsPrivateContractState( protocolDiamondAddress, { mockToken, mockTwinTokens }, - { sellers, DRs, agents, buyers, offers, groups } + { sellers, DRs, agents, buyers, offers, groups, twins } ) { /* - ProtocolLookups storage layout - - Variables marked with X have an external getter and are not handled here - #0 [ ] // placeholder for exchangeIdsByOffer - #1 [X] // placeholder for bundleIdByOffer - #2 [X] // placeholder for bundleIdByTwin - #3 [ ] // placeholder for groupIdByOffer - #4 [X] // placeholder for agentIdByOffer - #5 [X] // placeholder for sellerIdByAssistant - #6 [X] // placeholder for sellerIdByAdmin - #7 [X] // placeholder for sellerIdByClerk - #8 [ ] // placeholder for buyerIdByWallet - #9 [X] // placeholder for disputeResolverIdByAssistant - #10 [X] // placeholder for disputeResolverIdByAdmin - #11 [X] // placeholder for disputeResolverIdByClerk - #12 [ ] // placeholder for disputeResolverFeeTokenIndex - #13 [ ] // placeholder for agentIdByWallet - #14 [X] // placeholder for availableFunds - #15 [X] // placeholder for tokenList - #16 [ ] // placeholder for tokenIndexByAccount - #17 [ ] // placeholder for cloneAddress - #18 [ ] // placeholder for voucherCount - #19 [ ] // placeholder for conditionalCommitsByAddress - #20 [X] // placeholder for authTokenContracts - #21 [X] // placeholder for sellerIdByAuthToken - #22 [ ] // placeholder for twinRangesBySeller - #23 [ ] // placeholder for twinIdsByTokenAddressAndBySeller - #24 [X] // placeholder for twinReceiptsByExchange - #25 [X] // placeholder for allowedSellers - #26 [ ] // placeholder for allowedSellerIndex - #27 [X] // placeholder for exchangeCondition - #28 [ ] // placeholder for offerIdIndexByGroup - #29 [ ] // placeholder for pendingAddressUpdatesBySeller - #30 [ ] // placeholder for pendingAuthTokenUpdatesBySeller - #31 [ ] // placeholder for pendingAddressUpdatesByDisputeResolver - */ + ProtocolLookups storage layout + + Variables marked with X have an external getter and are not handled here + #0 [ ] // placeholder for exchangeIdsByOffer + #1 [X] // placeholder for bundleIdByOffer + #2 [X] // placeholder for bundleIdByTwin + #3 [ ] // placeholder for groupIdByOffer + #4 [X] // placeholder for agentIdByOffer + #5 [X] // placeholder for sellerIdByAssistant + #6 [X] // placeholder for sellerIdByAdmin + #7 [X] // placeholder for sellerIdByClerk + #8 [ ] // placeholder for buyerIdByWallet + #9 [X] // placeholder for disputeResolverIdByAssistant + #10 [X] // placeholder for disputeResolverIdByAdmin + #11 [X] // placeholder for disputeResolverIdByClerk + #12 [ ] // placeholder for disputeResolverFeeTokenIndex + #13 [ ] // placeholder for agentIdByWallet + #14 [X] // placeholder for availableFunds + #15 [X] // placeholder for tokenList + #16 [ ] // placeholder for tokenIndexByAccount + #17 [X] // placeholder for cloneAddress + #18 [ ] // placeholder for voucherCount + #19 [ ] // placeholder for conditionalCommitsByAddress + #20 [X] // placeholder for authTokenContracts + #21 [X] // placeholder for sellerIdByAuthToken + #22 [ ] // placeholder for twinRangesBySeller + #23 [ ] // placeholder for twinIdsByTokenAddressAndBySeller + #24 [X] // placeholder for twinReceiptsByExchange + #25 [X] // placeholder for allowedSellers + #26 [ ] // placeholder for allowedSellerIndex + #27 [X] // placeholder for exchangeCondition + #28 [ ] // placeholder for offerIdIndexByGroup + #29 [ ] // placeholder for pendingAddressUpdatesBySeller + #30 [ ] // placeholder for pendingAuthTokenUpdatesBySeller + #31 [ ] // placeholder for pendingAddressUpdatesByDisputeResolver + #32 [X] // placeholder for additionalCollections + #33 [ ] // placeholder for rangeIdByTwin + */ // starting slot const protocolLookupsSlot = keccak256(toUtf8Bytes("boson.protocol.lookups")); @@ -1244,11 +1336,11 @@ async function getProtocolLookupsPrivateContractState( const id = Number(offer.offer.id); // exchangeIdsByOffer let exchangeIdsByOffer = []; - const arraySlot = BigInt(getMappingStoragePosition(protocolLookupsSlotNumber + 0n, id, paddingType.START)); + const arraySlot = getMappingStoragePosition(protocolLookupsSlotNumber + 0n, id, paddingType.START); const arrayLength = await getStorageAt(protocolDiamondAddress, arraySlot); const arrayStart = keccak256(arraySlot); for (let i = 0n; i < arrayLength; i++) { - exchangeIdsByOffer.push(await getStorageAt(protocolDiamondAddress, arrayStart + i)); + exchangeIdsByOffer.push(await getStorageAt(protocolDiamondAddress, BigInt(arrayStart) + i)); } exchangeIdsByOfferState.push(exchangeIdsByOffer); @@ -1269,7 +1361,7 @@ async function getProtocolLookupsPrivateContractState( const accounts = [...sellers, ...DRs, ...agents, ...buyers]; for (const account of accounts) { - const accountAddress = account.wallet; + const accountAddress = account.wallet.address; // buyerIdByWallet buyerIdByWallet.push( @@ -1304,11 +1396,11 @@ async function getProtocolLookupsPrivateContractState( conditionalCommitsByAddress.push(commitsPerGroup); } - // disputeResolverFeeTokenIndex, tokenIndexByAccount, cloneAddress, voucherCount + // disputeResolverFeeTokenIndex, tokenIndexByAccount, voucherCount let disputeResolverFeeTokenIndex = []; let tokenIndexByAccount = []; - let cloneAddress = []; let voucherCount = []; + let cloneAddress = []; // all account ids const accountIds = accounts.map((account) => Number(account.id)); @@ -1374,9 +1466,9 @@ async function getProtocolLookupsPrivateContractState( await mockTwin.getAddress(), paddingType.START ); - const arrayLength = await getStorageAt(protocolDiamondAddress, arraySlot); - const arrayStart = keccak256(arraySlot); - for (let i = 0; i < arrayLength * 2n; i = i + 2n) { + const arrayLength = BigInt(await getStorageAt(protocolDiamondAddress, arraySlot)); + const arrayStart = BigInt(keccak256(arraySlot)); + for (let i = 0n; i < arrayLength * 2n; i = i + 2n) { // each BosonTypes.TokenRange has length 2 ranges[await mockTwin.getAddress()].push({ start: await getStorageAt(protocolDiamondAddress, arrayStart + i), @@ -1402,8 +1494,8 @@ async function getProtocolLookupsPrivateContractState( paddingType.START ); const arrayLength = await getStorageAt(protocolDiamondAddress, arraySlot); - const arrayStart = keccak256(arraySlot); - for (let i = 0; i < arrayLength; i++) { + const arrayStart = BigInt(keccak256(arraySlot)); + for (let i = 0n; i < arrayLength; i++) { twinIds[await mockTwin.getAddress()].push(await getStorageAt(protocolDiamondAddress, arrayStart + i)); } } @@ -1432,18 +1524,20 @@ async function getProtocolLookupsPrivateContractState( let offerIdIndexByGroup = []; for (const group of groups) { const id = group.id; - const firstMappingStorageSlot = BigInt(getMappingStoragePosition(protocolLookupsSlotNumber, id, paddingType.START)); - let offerInidices = []; + const firstMappingStorageSlot = BigInt( + getMappingStoragePosition(protocolLookupsSlotNumber + 28n, id, paddingType.START) + ); + let offerIndices = []; for (const offer of offers) { const id2 = Number(offer.offer.id); - offerInidices.push( + offerIndices.push( await getStorageAt( protocolDiamondAddress, getMappingStoragePosition(firstMappingStorageSlot, id2, paddingType.START) ) ); } - offerIdIndexByGroup.push(offerInidices); + offerIdIndexByGroup.push(offerIndices); } // pendingAddressUpdatesBySeller, pendingAuthTokenUpdatesBySeller, pendingAddressUpdatesByDisputeResolver @@ -1456,10 +1550,19 @@ async function getProtocolLookupsPrivateContractState( // pendingAddressUpdatesBySeller let structStorageSlot = BigInt(getMappingStoragePosition(protocolLookupsSlotNumber + 29n, id, paddingType.START)); let structFields = []; - for (let i = 0n; i < 5n; i++) { - // BosonTypes.Seller has 6 fields, but last bool is packed in one slot with previous field + for (let i = 0n; i < 6n; i++) { + // BosonTypes.Seller has 7 fields, but `address payable treasury` and `bool active` are packed into one slot structFields.push(await getStorageAt(protocolDiamondAddress, structStorageSlot + i)); } + const metadataUriLength = BigInt(await getStorageAt(protocolDiamondAddress, structStorageSlot + 6n)); + const metadataUriSlot = BigInt(ethersId(structStorageSlot + 6n)); + const occupiedSlots = metadataUriLength / 32n + 1n; + const metadataUri = []; + for (let i = 0n; i < occupiedSlots; i++) { + metadataUri.push(await getStorageAt(protocolDiamondAddress, metadataUriSlot + i)); + } + structFields.push(metadataUri); + pendingAddressUpdatesBySeller.push(structFields); // pendingAuthTokenUpdatesBySeller @@ -1478,10 +1581,22 @@ async function getProtocolLookupsPrivateContractState( // BosonTypes.DisputeResolver has 8 fields structFields.push(await getStorageAt(protocolDiamondAddress, structStorageSlot + i)); } - structFields[6] = await getStorageAt(protocolDiamondAddress, keccak256(structStorageSlot + 6n)); // represents field string metadataUri. Technically this value represents the length of the string, but since it should be 0, we don't do further decoding + structFields[6] = await getStorageAt(protocolDiamondAddress, keccak256(ethersId(structStorageSlot + 6n))); // represents field string metadataUri. Technically this value represents the length of the string, but since it should be 0, we don't do further decoding pendingAddressUpdatesByDisputeResolver.push(structFields); } + // rangeIdByTwin + let rangeIdByTwin = []; + for (const twin of twins) { + const { id } = twin; + rangeIdByTwin.push( + await getStorageAt( + protocolDiamondAddress, + getMappingStoragePosition(protocolLookupsSlotNumber + 33n, id, paddingType.START) + ) + ); + } + return { exchangeIdsByOfferState, groupIdByOfferState, @@ -1489,8 +1604,8 @@ async function getProtocolLookupsPrivateContractState( disputeResolverFeeTokenIndex, agentIdByWallet, tokenIndexByAccount, - cloneAddress, voucherCount, + cloneAddress, conditionalCommitsByAddress, twinRangesBySeller, twinIdsByTokenAddressAndBySeller, @@ -1499,6 +1614,7 @@ async function getProtocolLookupsPrivateContractState( pendingAddressUpdatesBySeller, pendingAuthTokenUpdatesBySeller, pendingAddressUpdatesByDisputeResolver, + rangeIdByTwin, }; } @@ -1511,7 +1627,7 @@ async function getStorageLayout(contractName) { return storage; } -function compareStorageLayouts(storageBefore, storageAfter) { +function compareStorageLayouts(storageBefore, storageAfter, equalCustomTypes) { // All old variables must be present in new layout in the same slots // New variables can be added if they don't affect the layout let storageOk = true; @@ -1527,7 +1643,7 @@ function compareStorageLayouts(storageBefore, storageAfter) { !stateVariableAfter || stateVariableAfter.slot != stateVariableBefore.slot || stateVariableAfter.offset != stateVariableBefore.offset || - stateVariableAfter.type != stateVariableBefore.type + compareTypes(stateVariableAfter.type, stateVariableBefore.type, equalCustomTypes) ) { storageOk = false; console.error("Storage layout mismatch"); @@ -1539,26 +1655,36 @@ function compareStorageLayouts(storageBefore, storageAfter) { return storageOk; } +// Sometimes struct labels change even if the structs are the same +// In those cases, manually add the new label to the equalCustomTypes object +function compareTypes(variableTypeAfter, variableTypeBefore, equalCustomTypes) { + if (variableTypeBefore == variableTypeAfter) return false; + + for (const [oldLabel, newLabel] of Object.entries(equalCustomTypes)) { + variableTypeBefore = variableTypeBefore.replaceAll(oldLabel, newLabel); + } + + return variableTypeAfter != variableTypeBefore; +} + async function populateVoucherContract( deployer, protocolDiamondAddress, - { accountHandler, exchangeHandler, offerHandler, fundsHandler }, + { accountHandler, exchangeHandler, offerHandler, fundsHandler, groupHandler }, { mockToken }, existingEntities, isBefore = false ) { - let DR; + let DRs; let sellers = []; let buyers = []; let offers = []; let bosonVouchers = []; let exchanges = []; - let voucherIndex = 1; - if (existingEntities) { // If existing entities are provided, we use them instead of creating new ones - ({ DR, sellers, buyers, offers, bosonVouchers } = existingEntities); + ({ DRs, sellers, buyers, offers, bosonVouchers } = existingEntities); } else { const entityType = { SELLER: 0, @@ -1596,10 +1722,14 @@ async function populateVoucherContract( // create entities switch (entity) { case entityType.DR: { + const clerkAddress = versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion) + ? wallet.address + : ZeroAddress; + const disputeResolver = mockDisputeResolver( await wallet.getAddress(), await wallet.getAddress(), - await wallet.getAddress(), + clerkAddress, await wallet.getAddress(), true, true @@ -1615,13 +1745,14 @@ async function populateVoucherContract( await accountHandler .connect(connectedWallet) .createDisputeResolver(disputeResolver, disputeResolverFees, sellerAllowList); - DR = { + + DRs.push({ wallet: connectedWallet, id: disputeResolver.id, disputeResolver, disputeResolverFees, sellerAllowList, - }; + }); if (versionsWithActivateDRFunction.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion)) { //ADMIN role activates Dispute Resolver @@ -1645,11 +1776,14 @@ async function populateVoucherContract( let authToken = mockAuthToken(); // set unique new voucherInitValues - const voucherInitValues = new VoucherInitValues(`http://seller${id}.com/uri`, id * 10); - await accountHandler.connect(connectedWallet).createSeller(seller, authToken, voucherInitValues); - - // calculate voucher contract address and cast it to contract instance - const voucherContractAddress = calculateContractAddress(await accountHandler.getAddress(), voucherIndex++); + const voucherInitValues = versionsBelowV2_3.includes( + isBefore ? versionTags.oldVersion : versionTags.newVersion + ) + ? new VoucherInitValues(`http://seller${id}.com/uri`, id * 10) + : new VoucherInitValues(`http://seller${id}.com/uri`, id * 10, ZeroHash); + const tx = await accountHandler.connect(connectedWallet).createSeller(seller, authToken, voucherInitValues); + const receipt = await tx.wait(); + const [, , voucherContractAddress] = receipt.logs.find((e) => e?.fragment?.name === "SellerCreated").args; const bosonVoucher = await getContractAt("BosonVoucher", voucherContractAddress); sellers.push({ @@ -1707,12 +1841,12 @@ async function populateVoucherContract( offerDates.validUntil = (BigInt(now) + oneMonth * 6n * BigInt(offerId + 1)).toString(); // Set unique offerDurations based on offer id - offerDurations.disputePeriod = `${(offerId + 1) * oneMonth}`; - offerDurations.voucherValid = `${(offerId + 1) * oneMonth}`; - offerDurations.resolutionPeriod = `${(offerId + 1) * oneDay}`; + offerDurations.disputePeriod = `${(offerId + 1) * Number(oneMonth)}`; + offerDurations.voucherValid = `${(offerId + 1) * Number(oneMonth)}`; + offerDurations.resolutionPeriod = `${(offerId + 1) * Number(oneDay)}`; // choose one DR and agent - const disputeResolverId = DR.disputeResolver.id; + const disputeResolverId = DRs[0].disputeResolver.id; const agentId = "0"; // create an offer @@ -1739,7 +1873,7 @@ async function populateVoucherContract( let exchangeId = Number(await exchangeHandler.getNextExchangeId()); for (let i = 0; i < buyers.length; i++) { for (let j = i; j < buyers.length; j++) { - const offer = offers[i + j].offer; // some offers will be picked multiple times, some never. + const { offer, groupId } = offers[i + j]; // some offers will be picked multiple times, some never. const offerPrice = offer.price; const buyerWallet = buyers[j].wallet; let msgValue; @@ -1751,15 +1885,34 @@ async function populateVoucherContract( await mockToken.connect(buyerWallet).approve(protocolDiamondAddress, offerPrice); await mockToken.mint(await buyerWallet.getAddress(), offerPrice); } - await exchangeHandler - .connect(buyerWallet) - .commitToOffer(await buyerWallet.getAddress(), offer.id, { value: msgValue }); + + // v2.3.0 introduces commitToConditionalOffer method which should be used for conditional offers + const isAfterV2_3_0 = !versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion); + if (groupId && isAfterV2_3_0) { + // get condition + decache("../../scripts/domain/Condition.js"); + Condition = require("../../scripts/domain/Condition.js"); + let [, , condition] = await groupHandler.getGroup(groupId); + condition = Condition.fromStruct(condition); + + // commit to conditional offer + await exchangeHandler + .connect(buyerWallet) + .commitToConditionalOffer(await buyerWallet.getAddress(), offer.id, condition.minTokenId, { + value: msgValue, + }); + } else { + await exchangeHandler + .connect(buyerWallet) + .commitToOffer(await buyerWallet.getAddress(), offer.id, { value: msgValue }); + } + exchanges.push({ exchangeId: exchangeId, offerId: offer.id, buyerIndex: j }); exchangeId++; } } - return { DR, sellers, buyers, offers, exchanges, bosonVouchers }; + return { DRs, sellers, buyers, offers, exchanges, bosonVouchers }; } async function getVoucherContractState({ bosonVouchers, exchanges, sellers, buyers }) { @@ -1767,7 +1920,8 @@ async function getVoucherContractState({ bosonVouchers, exchanges, sellers, buye for (const bosonVoucher of bosonVouchers) { // supports interface const interfaceIds = await getInterfaceIds(false); - const suppportstInterface = await Promise.all( + + const supportstInterface = await Promise.all( [interfaceIds["IBosonVoucher"], interfaceIds["IERC721"], interfaceIds["IERC2981"]].map((i) => bosonVoucher.supportsInterface(i) ) @@ -1809,7 +1963,7 @@ async function getVoucherContractState({ bosonVouchers, exchanges, sellers, buye ); bosonVouchersState.push({ - suppportstInterface, + supportstInterface, sellerId, contractURI, getRoyaltyPercentage, @@ -1828,9 +1982,9 @@ async function getVoucherContractState({ bosonVouchers, exchanges, sellers, buye } function revertState() { - shell.exec(`rm -rf contracts/* scripts/*`); - shell.exec(`git checkout HEAD contracts scripts`); - shell.exec(`git reset HEAD contracts scripts`); + shell.exec(`rm -rf contracts/* scripts/* package.json package-lock.json`); + shell.exec(`git checkout HEAD contracts scripts package.json package-lock.json`); + shell.exec(`git reset HEAD contracts scripts package.json package-lock.json`); } async function getDisputeResolver(accountHandler, value, { getBy }) { diff --git a/test/util/utils.js b/test/util/utils.js index cd0b08911..ba21389d1 100644 --- a/test/util/utils.js +++ b/test/util/utils.js @@ -307,6 +307,7 @@ const paddingType = { function getMappingStoragePosition(slot, key, padding = paddingType.NONE) { let keyBuffer; + let keyHex = String(key).startsWith("0x") ? String(key) : toHexString(key); switch (padding) {