From 5166a27e45818c82f551cd90c9a92f21137a0366 Mon Sep 17 00:00:00 2001 From: Klemen <64400885+zajck@users.noreply.github.com> Date: Fri, 8 Mar 2024 09:08:11 +0100 Subject: [PATCH] 2.4.0 migration script and upgrade tests (#923) * Migration script * Back-fill the data * Fix migration script * Handle royalty info in upgrade tests * generic upgrade tests * exchanges, sellers, offers new method tests * Commit to price discovery offer * Sequential commit to offer * new methods: voucher, pdclient; fixes: funds * offer creation requires fee limit * Release funds old exchanges * Metatx method allowlisting * simplify isAllowlisted check * private protocol variables * validate incoming voucher private state * Enable back skipped tests * initialization by parts * Do not skip expired/voided offers * Apply suggestions from code review Co-authored-by: albertfolch-redeemeum <102516373+albertfolch-redeemeum@users.noreply.github.com> --------- Co-authored-by: albertfolch-redeemeum <102516373+albertfolch-redeemeum@users.noreply.github.com> --- .../ProtocolInitializationHandlerFacet.sol | 4 +- hardhat.config.js | 14 + scripts/config/role-assignments.js | 2 +- scripts/migrations/migrate_2.4.0.js | 297 ++++ scripts/upgrade-facets.js | 14 +- scripts/upgrade-hooks/2.4.0.js | 125 ++ test/protocol/SequentialCommitHandlerTest.js | 2 +- test/upgrade/00_config.js | 6 + test/upgrade/01_generic.js | 7 + test/upgrade/2.3.0-2.4.0.js | 1398 +++++++++++++++++ test/upgrade/clients/01_generic.js | 4 +- test/util/mock.js | 49 +- test/util/upgrade.js | 249 ++- 13 files changed, 2110 insertions(+), 61 deletions(-) create mode 100644 scripts/migrations/migrate_2.4.0.js create mode 100644 scripts/upgrade-hooks/2.4.0.js create mode 100644 test/upgrade/2.3.0-2.4.0.js diff --git a/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol b/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol index da324f4ab..7d1ca3894 100644 --- a/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol +++ b/contracts/protocol/facets/ProtocolInitializationHandlerFacet.sol @@ -197,7 +197,7 @@ contract ProtocolInitializationHandlerFacet is IBosonProtocolInitializationHandl * - Length of _sellerIds, _royaltyPercentages and _offerIds arrays do not match * - Any of the offerIds does not exist * - * @param _initializationData - data representing uint256[] _sellerIds, uint256[] _royaltyPercentages, uint256[][] _offerIds, address _priceDiscovery + * @param _initializationData - data representing uint256[] _royaltyPercentages, uint256[][] _sellerIds, uint256[][] _offerIds, address _priceDiscovery */ function initV2_4_0(bytes calldata _initializationData) internal { // Current version must be 2.3.0 @@ -249,7 +249,7 @@ contract ProtocolInitializationHandlerFacet is IBosonProtocolInitializationHandl * This method should not be registered as a diamond public method. * Refer to initV2_4_0 for more details about the data structure. * - * @param _initializationData - data representing uint256[] _sellerIds, uint256[] _royaltyPercentages, uint256[][] _offerIds + * @param _initializationData - data representing uint256[] _royaltyPercentages, uint256[][] _sellerIds, uint256[][] _offerIds, address _priceDiscovery */ function initV2_4_0External(bytes calldata _initializationData) external onlyRole(UPGRADER) { initV2_4_0(_initializationData); diff --git a/hardhat.config.js b/hardhat.config.js index 2bd40eac3..eac4fcd5b 100644 --- a/hardhat.config.js +++ b/hardhat.config.js @@ -204,6 +204,20 @@ module.exports = { }, viaIR: true, }, + { + version: "0.8.21", + settings: { + viaIR: false, + optimizer: { + enabled: true, + runs: 200, + details: { + yul: true, + }, + }, + evmVersion: "london", // for ethereum mainnet, use shanghai, for polygon, use london + }, + }, { version: "0.8.22", settings: { diff --git a/scripts/config/role-assignments.js b/scripts/config/role-assignments.js index b3ef36073..6a76a87af 100644 --- a/scripts/config/role-assignments.js +++ b/scripts/config/role-assignments.js @@ -82,7 +82,7 @@ exports.RoleAssignments = { localhost: { AdminAddress: { // do not change name - roles: [Role.ADMIN, Role.UPGRADER], + roles: [Role.ADMIN, Role.UPGRADER, Role.PAUSER], }, // For minting vouchers diff --git a/scripts/migrations/migrate_2.4.0.js b/scripts/migrations/migrate_2.4.0.js new file mode 100644 index 000000000..b667be596 --- /dev/null +++ b/scripts/migrations/migrate_2.4.0.js @@ -0,0 +1,297 @@ +const shell = require("shelljs"); + +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, writeContracts, deploymentComplete } = require("../util/utils.js"); +const hre = require("hardhat"); +const PausableRegion = require("../domain/PausableRegion.js"); +const ethers = hre.ethers; +const { getContractAt, getSigners, ZeroAddress } = ethers; +const network = hre.network.name; +const abiCoder = new ethers.AbiCoder(); +const tag = "HEAD"; +const version = "2.4.0"; +const { EXCHANGE_ID_2_2_0, WrappedNative } = 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 v2.4.0-rc.3 + addOrUpgrade: [ + "AccountHandlerFacet", + "AgentHandlerFacet", + "BundleHandlerFacet", + "BuyerHandlerFacet", + "ConfigHandlerFacet", + "DisputeHandlerFacet", + "DisputeResolverHandlerFacet", + "ExchangeHandlerFacet", + "FundsHandlerFacet", + "GroupHandlerFacet", + "MetaTransactionsHandlerFacet", + "OfferHandlerFacet", + "OrchestrationHandlerFacet1", + "OrchestrationHandlerFacet2", + "PauseHandlerFacet", + "ProtocolInitializationHandlerFacet", + "SellerHandlerFacet", + "TwinHandlerFacet", + "PriceDiscoveryHandlerFacet", + "SequentialCommitHandlerFacet", + ], + remove: [], + skipSelectors: {}, + facetsToInit: { + ExchangeHandlerFacet: { init: [], constructorArgs: [EXCHANGE_ID_2_2_0[network]] }, // must match nextExchangeId at the time of the upgrade + AccountHandlerFacet: { init: [] }, + DisputeResolverHandlerFacet: { init: [] }, + OfferHandlerFacet: { init: [] }, + OrchestrationHandlerFacet1: { init: [] }, + PriceDiscoveryHandlerFacet: { init: [], constructorArgs: [WrappedNative[network]] }, + SequentialCommitHandlerFacet: { init: [], constructorArgs: [WrappedNative[network]] }, + }, + initializationData: abiCoder.encode( + ["uint256[]", "uint256[][]", "uint256[][]", "address"], + [[], [], [], ZeroAddress] + ), // dummy; populated in migrate script +}; + +async function migrate(env, params) { + console.log(`Migration ${tag} started`); + + if (params) { + if (params.WrappedNative) { + console.log("Using WrappedNative from params"); + config.facetsToInit.PriceDiscoveryHandlerFacet.constructorArgs[0] = params.WrappedNative; + config.facetsToInit.SequentialCommitHandlerFacet.constructorArgs[0] = params.WrappedNative; + } + } + + try { + 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.3.0") { + throw new Error("Current contract version must be 2.3.0"); + } + + let contracts = contractsFile?.contracts; + + // Get addresses of currently deployed contracts + const accessControllerAddress = contracts.find((c) => c.name === "AccessController")?.address; + + const accessController = await getContractAt("AccessController", accessControllerAddress); + + 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; + + if (env !== "upgrade-test") { + // Checking old version contracts to get selectors to remove + console.log("Checking out contracts on version 2.3.0"); + shell.exec(`rm -rf contracts/*`); + shell.exec(`git checkout v2.3.0 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"); + } + + console.log("Pausing the Seller, Offer and Exchanges region..."); + let pauseHandler = await getContractAt("IBosonPauseHandler", protocolAddress); + + const pauseTransaction = await pauseHandler.pause( + [PausableRegion.Sellers, PausableRegion.Offers, PausableRegion.Exchanges], + await getFees(maxPriorityFeePerGas) + ); + + // await 1 block to ensure the pause is effective + await pauseTransaction.wait(confirmations); + + let functionNamesToSelector = {}; + + const preUpgradeFacetList = config.addOrUpgrade.filter( + (f) => !["PriceDiscoveryHandlerFacet", "SequentialCommitHandlerFacet"].includes(f) + ); + for (const facet of preUpgradeFacetList) { + const facetContract = await getContractAt(facet, protocolAddress); + const { signatureToNameMapping } = getSelectors(facetContract, true); + functionNamesToSelector = { ...functionNamesToSelector, ...signatureToNameMapping }; + } + + let getFunctionHashesClosure = getStateModifyingFunctionsHashes(preUpgradeFacetList, ["executeMetaTransaction"]); + + const oldSelectors = await getFunctionHashesClosure(); + + // Get the data for back-filling + const { sellerIds, royaltyPercentages, offerIds } = await prepareInitializationData(protocolAddress); + const { backFillData } = require("../../scripts/upgrade-hooks/2.4.0.js"); + backFillData({ sellerIds, royaltyPercentages, offerIds }); + + console.log(`Checking out contracts on version ${tag}`); + shell.exec(`rm -rf 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"); + // 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 {} + + // Deploy Boson Price Discovery Client + console.log("Deploying Boson Price Discovery Client..."); + const constructorArgs = [WrappedNative[network], protocolAddress]; + const bosonPriceDiscoveryFactory = await ethers.getContractFactory("BosonPriceDiscovery"); + const bosonPriceDiscovery = await bosonPriceDiscoveryFactory.deploy(...constructorArgs); + await bosonPriceDiscovery.waitForDeployment(); + + deploymentComplete( + "BosonPriceDiscoveryClient", + await bosonPriceDiscovery.getAddress(), + constructorArgs, + "", + contracts + ); + + await writeContracts(contracts, env, version); + + console.log("Executing upgrade facets script"); + await hre.run("upgrade-facets", { + env, + facetConfig: JSON.stringify(config), + newVersion: version, + functionNamesToSelector: JSON.stringify(functionNamesToSelector), + }); + + getFunctionHashesClosure = getStateModifyingFunctionsHashes(config.addOrUpgrade, ["executeMetaTransaction"]); + + const newSelectors = await getFunctionHashesClosure(); + const unchanged = oldSelectors.filter((value) => newSelectors.includes(value)); + const selectorsToRemove = oldSelectors.filter((value) => !unchanged.includes(value)); // unique old selectors + const selectorsToAdd = newSelectors.filter((value) => !unchanged.includes(value)); // unique new selectors + + 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 isAllowed = await metaTransactionHandlerFacet["isFunctionAllowlisted(bytes32)"](selector); + if (isAllowed) { + console.error(`Selector ${selector} was not removed`); + } + } + + console.log("Adding selectors", selectorsToAdd.join(",")); + await metaTransactionHandlerFacet.setAllowlistedFunctions(selectorsToAdd, true); + + 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("Unpausing all regions..."); + pauseHandler = await getContractAt("IBosonPauseHandler", protocolAddress); + await pauseHandler.unpause([], await getFees(maxPriorityFeePerGas)); + + shell.exec(`git checkout HEAD`); + shell.exec(`git reset HEAD`); + console.log(`Migration ${tag} completed`); + } catch (e) { + console.error(e); + shell.exec(`git checkout HEAD`); + shell.exec(`git reset HEAD`); + throw `Migration failed with: ${e}`; + } +} + +async function prepareInitializationData(protocolAddress) { + const sellerHandler = await getContractAt("IBosonAccountHandler", protocolAddress); + const nextSellerId = await sellerHandler.getNextAccountId(); + + const collections = {}; + const royaltyPercentageToSellersAndOffers = {}; + for (let i = 1n; i < nextSellerId; i++) { + const [exists] = await sellerHandler.getSeller(i); + if (!exists) { + continue; + } + const [defaultVoucherAddress, additionalCollections] = await sellerHandler.getSellersCollections(i); + const allCollections = [defaultVoucherAddress, ...additionalCollections]; + const bosonVouchers = await Promise.all( + allCollections.map((collectionAddress) => getContractAt("IBosonVoucher", collectionAddress)) + ); + const royaltyPercentages = await Promise.all(bosonVouchers.map((voucher) => voucher.getRoyaltyPercentage())); + collections[i] = royaltyPercentages; + + for (const rp of royaltyPercentages) { + if (!royaltyPercentageToSellersAndOffers[rp]) { + royaltyPercentageToSellersAndOffers[rp] = { sellers: [], offers: [] }; + } + } + + // Find the minimum royalty percentage + const minRoyaltyPercentage = royaltyPercentages.reduce((a, b) => Math.min(a, b)); + royaltyPercentageToSellersAndOffers[minRoyaltyPercentage].sellers.push(i); + } + + const offerHandler = await getContractAt("IBosonOfferHandler", protocolAddress); + const nextOfferId = await offerHandler.getNextOfferId(); + + for (let i = 1n; i < nextOfferId; i++) { + const [, offer] = await offerHandler.getOffer(i); + + const collectionIndex = offer.collectionIndex; + const sellerId = offer.sellerId; + const offerId = i; + + const offerRoyaltyPercentage = collections[sellerId][collectionIndex]; + + // Guaranteed to exist + royaltyPercentageToSellersAndOffers[offerRoyaltyPercentage].offers.push(offerId); + } + + const royaltyPercentages = Object.keys(royaltyPercentageToSellersAndOffers); + const sellersAndOffers = Object.values(royaltyPercentageToSellersAndOffers); + const sellerIds = sellersAndOffers.map((sellerAndOffer) => sellerAndOffer.sellers); + const offerIds = sellersAndOffers.map((sellerAndOffer) => sellerAndOffer.offers); + return { royaltyPercentages, sellerIds, offerIds }; +} + +exports.migrate = migrate; diff --git a/scripts/upgrade-facets.js b/scripts/upgrade-facets.js index 0092e4487..ffd7860b1 100644 --- a/scripts/upgrade-facets.js +++ b/scripts/upgrade-facets.js @@ -127,13 +127,23 @@ async function main(env, facetConfig, version, functionNamesToSelector) { facets = await getFacets(); } + let deployedFacets = []; if (preUpgrade) { console.log("\n📋Running pre-upgrade script..."); - facets = await preUpgrade(protocolAddress, facets); + const { facets: preUpgradeModifiedFacets, deployedFacets: preUpgradeDeployedFacets } = await preUpgrade( + protocolAddress, + facets, + env + ); + facets = preUpgradeModifiedFacets; + deployedFacets = preUpgradeDeployedFacets; } // Deploy new facets - let deployedFacets = await deployProtocolFacets(facets.addOrUpgrade, facets.facetsToInit, maxPriorityFeePerGas); + deployedFacets = [ + ...deployedFacets, + ...(await deployProtocolFacets(facets.addOrUpgrade, facets.facetsToInit, maxPriorityFeePerGas)), + ]; // Cast Diamond to DiamondCutFacet, DiamondLoupeFacet and IERC165Extended const diamondCutFacet = await getContractAt("DiamondCutFacet", protocolAddress); diff --git a/scripts/upgrade-hooks/2.4.0.js b/scripts/upgrade-hooks/2.4.0.js new file mode 100644 index 000000000..ddc910c26 --- /dev/null +++ b/scripts/upgrade-hooks/2.4.0.js @@ -0,0 +1,125 @@ +const hre = require("hardhat"); +const { ethers } = hre; +const { getContractAt, getContractFactory, ZeroAddress } = ethers; +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 = BigInt(tipSuggestion) * tipMultiplier; +const { getFees, readContracts } = require("./../util/utils"); +const confirmations = hre.network.name == "hardhat" ? 1 : environments.confirmations; +const abiCoder = new ethers.AbiCoder(); +const network = hre.network.name; + +let backFillData = {}; + +async function preUpgrade(protocolAddress, facets, env) { + const { sellerIds, royaltyPercentages, offerIds } = backFillData; + console.log("Backfilling sellers and offers..."); + console.log("royaltyPercentages", royaltyPercentages); + console.log("sellerIds", sellerIds); + console.log("offerIds", offerIds); + + const maxBackfill = 25; // max number of sellers and offers to backfill in a single transaction + + // Backfill the data pre-upgrade + let totalCount = + sellerIds.reduce((acc, val) => acc + val.length, 0) + offerIds.reduce((acc, val) => acc + val.length, 0); + let backfillCount = Math.floor(totalCount / maxBackfill); + + let deployedFacets = []; + if (backfillCount > 0) { + const diamondCutFacet = await getContractAt("DiamondCutFacet", protocolAddress); + const protocolInitializationContractFactory = await getContractFactory("ProtocolInitializationHandlerFacet"); + const protocolInitializationFacet = await protocolInitializationContractFactory.deploy( + await getFees(maxPriorityFeePerGas) + ); + await protocolInitializationFacet.waitForDeployment(confirmations); + + const deployedFacet = { + name: "ProtocolInitializationHandlerFacet", + contract: protocolInitializationFacet, + cut: [], + constructorArgs: [], + }; + + deployedFacets.push(deployedFacet); + + facets.addOrUpgrade = facets.addOrUpgrade.filter((f) => f !== "ProtocolInitializationHandlerFacet"); + + while (totalCount >= maxBackfill) { + // need to backfill with `initV2_4_0External` + + let sliceCount = 0; + const royaltyPercentagesSlice = []; + const sellerIdsSlice = []; + const offerIdsSlice = []; + + while (sliceCount < maxBackfill) { + royaltyPercentagesSlice.push(royaltyPercentages[0]); + const sellerIdsSliceByPercentage = sellerIds[0].slice(0, maxBackfill - sliceCount); + + sellerIds[0] = sellerIds[0].slice(maxBackfill - sliceCount); + + const remainingSlots = Math.max(maxBackfill - sliceCount - sellerIdsSliceByPercentage.length, 0); + + sliceCount += sellerIdsSliceByPercentage.length; + + const offerIdsSliceByPercentage = offerIds[0].slice(0, remainingSlots); + offerIds[0] = offerIds[0].slice(remainingSlots); + + sliceCount += offerIdsSliceByPercentage.length; + + if (sellerIds[0].length == 0 && offerIds[0].length == 0) { + sellerIds.shift(); + offerIds.shift(); + royaltyPercentages.shift(); + } + + sellerIdsSlice.push(sellerIdsSliceByPercentage); + offerIdsSlice.push(offerIdsSliceByPercentage); + } + + totalCount -= sliceCount; + + const initializationData = abiCoder.encode( + ["uint256[]", "uint256[][]", "uint256[][]", "address"], + [royaltyPercentagesSlice, sellerIdsSlice, offerIdsSlice, ZeroAddress] + ); + + console.log(`Backfilling sellers and offers... #${backfillCount--}`); + console.log([royaltyPercentagesSlice, sellerIdsSlice, offerIdsSlice, ZeroAddress]); + const calldataProtocolInitialization = protocolInitializationFacet.interface.encodeFunctionData( + "initV2_4_0External", + [initializationData] + ); + + // Make the "cut", i.e. call initV2_4_0External via diamond + await diamondCutFacet.diamondCut( + [], + await protocolInitializationFacet.getAddress(), + calldataProtocolInitialization, + await getFees(maxPriorityFeePerGas) + ); + } + } + + // Prepare initialization data + // Backfill the remaining data + const { chainId } = await ethers.provider.getNetwork(); + const contractsFile = readContracts(chainId, network, env); + let contracts = contractsFile?.contracts; + const priceDiscoveryClientAddress = contracts.find((c) => c.name === "BosonPriceDiscoveryClient")?.address; + console.log("remaining data"); + console.log([royaltyPercentages, sellerIds, offerIds, priceDiscoveryClientAddress]); + facets.initializationData = abiCoder.encode( + ["uint256[]", "uint256[][]", "uint256[][]", "address"], + [royaltyPercentages, sellerIds, offerIds, priceDiscoveryClientAddress] + ); + + return { facets, deployedFacets }; +} + +exports.preUpgrade = preUpgrade; +exports.backFillData = function (data) { + backFillData = data; +}; diff --git a/test/protocol/SequentialCommitHandlerTest.js b/test/protocol/SequentialCommitHandlerTest.js index 89e868c7a..edcc75770 100644 --- a/test/protocol/SequentialCommitHandlerTest.js +++ b/test/protocol/SequentialCommitHandlerTest.js @@ -336,7 +336,7 @@ describe("IBosonSequentialCommitHandler", function () { it("should emit FundsEncumbered, FundsReleased, FundsWithdrawn and BuyerCommitted events", async function () { // Sequential commit to offer, retrieving the event - const tx = sequentialCommitHandler + const tx = await sequentialCommitHandler .connect(buyer2) .sequentialCommitToOffer(buyer2.address, tokenId, priceDiscovery); diff --git a/test/upgrade/00_config.js b/test/upgrade/00_config.js index c75265d5f..ad8d92930 100644 --- a/test/upgrade/00_config.js +++ b/test/upgrade/00_config.js @@ -76,6 +76,7 @@ async function getFacets() { "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 + "v2.3.0": v2_2_0, // same as v2.2.0 }, upgrade: { "v2.1.0": { @@ -219,6 +220,11 @@ const tagsByVersion = { newVersion: "v2.3.0", updateDomain: ["Condition"], }, + "2.4.0": { + oldVersion: "v2.3.0", + newVersion: "HEAD", + updateDomain: ["Offer"], + }, }; exports.getFacets = getFacets; diff --git a/test/upgrade/01_generic.js b/test/upgrade/01_generic.js index 338715d0a..112f401cb 100644 --- a/test/upgrade/01_generic.js +++ b/test/upgrade/01_generic.js @@ -241,6 +241,13 @@ function getGenericContext( const offerFeeLimit = MaxUint256; // unlimited offer fee to not affect the tests const seller = preUpgradeEntities.sellers[2]; + offer.royaltyInfo = [ + { + bps: [`${seller.voucherInitValues.royaltyPercentage}`], + recipients: [ZeroAddress], + }, + ]; + offerHandler = contractsAfter.offerHandler; await offerHandler .connect(seller.wallet) diff --git a/test/upgrade/2.3.0-2.4.0.js b/test/upgrade/2.3.0-2.4.0.js new file mode 100644 index 000000000..0a2b32caf --- /dev/null +++ b/test/upgrade/2.3.0-2.4.0.js @@ -0,0 +1,1398 @@ +const shell = require("shelljs"); +const hre = require("hardhat"); +const ethers = hre.ethers; +const { ZeroAddress, encodeBytes32String, id, MaxUint256, getContractFactory, getContractAt, provider, parseUnits } = + ethers; +const { assert, expect } = require("chai"); + +const { Collection, CollectionList } = require("../../scripts/domain/Collection"); +const { RoyaltyRecipientInfo, RoyaltyRecipientInfoList } = require("../../scripts/domain/RoyaltyRecipientInfo.js"); +const { RoyaltyInfo } = require("../../scripts/domain/RoyaltyInfo"); +const PriceDiscovery = require("../../scripts/domain/PriceDiscovery"); +const Side = require("../../scripts/domain/Side"); +const PriceType = require("../../scripts/domain/PriceType.js"); +const Voucher = require("../../scripts/domain/Voucher.js"); +const TokenType = require("../../scripts/domain/TokenType"); +const EvaluationMethod = require("../../scripts/domain/EvaluationMethod"); +const { RevertReasons } = require("../../scripts/config/revert-reasons"); + +const { + getSnapshot, + revertToSnapshot, + getEvent, + calculateCloneAddress, + deriveTokenId, + compareRoyaltyInfo, + calculateVoucherExpiry, + applyPercentage, +} = require("../util/utils"); +const { + mockOffer, + mockExchange, + mockVoucher, + mockBuyer, + mockSeller, + mockCondition, + mockAuthToken, + mockVoucherInitValues, + mockTwin, +} = require("../util/mock"); +const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens.js"); +const { getStateModifyingFunctionsHashes } = require("../../scripts/util/diamond-utils.js"); + +const { + deploySuite, + populateProtocolContract, + getProtocolContractState, + revertState, + getStorageLayout, + getVoucherContractState, + populateVoucherContract, +} = require("../util/upgrade"); +const { getGenericContext } = require("./01_generic"); +const { getGenericContext: getGenericContextVoucher } = require("./clients/01_generic"); + +const version = "2.4.0"; +const { migrate } = require(`../../scripts/migrations/migrate_${version}.js`); + +/** + * Upgrade test case - After upgrade from 2.3.0 to 2.4.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, buyer, other1, other2, other3, other4, other5, other6; + let accountHandler, + configHandler, + exchangeHandler, + twinHandler, + offerHandler, + fundsHandler, + priceDiscoveryHandler, + sequentialCommitHandler, + disputeHandler, + orchestrationHandler, + groupHandler, + metaTransactionsHandler; + let snapshot; + let protocolDiamondAddress, mockContracts; + let contractsAfter; + let protocolContractStateBefore, protocolContractStateAfter; + let weth; + 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, buyer, other1, other2, other3, other4, other5, other6] = await ethers.getSigners(); + + let contractsBefore; + + ({ + protocolDiamondAddress, + protocolContracts: contractsBefore, + mockContracts, + } = await deploySuite(deployer, version)); + + // Populate protocol with data + preUpgradeEntities = await populateProtocolContract( + deployer, + protocolDiamondAddress, + contractsBefore, + mockContracts, + true + ); + + 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, + { + isBefore: true, + skipFacets: [ + "IBosonPriceDiscoveryHandler", + "IBosonSequentialCommitHandler", + "PriceDiscoveryHandlerFacet", + "SequentialCommitHandlerFacet", + ], + } + ); + + const { offers } = preUpgradeEntities; + for (let offerState of protocolContractStateBefore.offerContractState.offersState) { + offerState[1]["priceType"] = PriceType.Static; + offerState[1]["royaltyInfo"] = offers[Number(offerState[1].id) - 1].royaltyInfo; + } + + const voucherContractState = await getVoucherContractState(preUpgradeEntitiesVoucher); + + ({ exchangeHandler, twinHandler } = contractsBefore); + + let getFunctionHashesClosure = getStateModifyingFunctionsHashes( + ["OrchestrationHandlerFacet1", "OfferHandlerFacet", "ConfigHandlerFacet", "ExchangeHandlerFacet"], + undefined, + ["createSellerAnd", "createOffer", "createPremintedOffer", "setMaxRoyaltyPecentage", "commitToPreMintedOffer"] + ); + + removedFunctionHashes = await getFunctionHashesClosure(); + + shell.exec(`git checkout HEAD scripts`); + + // Add WETH + const wethFactory = await getContractFactory("WETH9"); + weth = await wethFactory.deploy(); + await weth.waitForDeployment(); + + await migrate("upgrade-test", { WrappedNative: await weth.getAddress() }); + + // Cast to updated interface + let newHandlers = { + accountHandler: "IBosonAccountHandler", + pauseHandler: "IBosonPauseHandler", + configHandler: "IBosonConfigHandler", + offerHandler: "IBosonOfferHandler", + groupHandler: "IBosonGroupHandler", + orchestrationHandler: "IBosonOrchestrationHandler", + fundsHandler: "IBosonFundsHandler", + exchangeHandler: "IBosonExchangeHandler", + priceDiscoveryHandler: "IBosonPriceDiscoveryHandler", + sequentialCommitHandler: "IBosonSequentialCommitHandler", + disputeHandler: "IBosonDisputeHandler", + metaTransactionsHandler: "IBosonMetaTransactionsHandler", + }; + + contractsAfter = { ...contractsBefore }; + + for (const [handlerName, interfaceName] of Object.entries(newHandlers)) { + contractsAfter[handlerName] = await ethers.getContractAt(interfaceName, protocolDiamondAddress); + } + + ({ + accountHandler, + configHandler, + exchangeHandler, + offerHandler, + fundsHandler, + priceDiscoveryHandler, + sequentialCommitHandler, + disputeHandler, + orchestrationHandler, + groupHandler, + metaTransactionsHandler, + } = contractsAfter); + + getFunctionHashesClosure = getStateModifyingFunctionsHashes( + [ + "OrchestrationHandlerFacet1", + "OfferHandlerFacet", + "SellerHandlerFacet", + "ConfigHandlerFacet", + "PriceDiscoveryHandlerFacet", + "SequentialCommitHandlerFacet", + "ExchangeHandlerFacet", + ], + undefined, + [ + "createSellerAnd", + "createOffer", + "createPremintedOffer", + "addRoyaltyRecipients", + "updateRoyaltyRecipients", + "removeRoyaltyRecipients", + "setPriceDiscoveryAddress", + "setMaxRoyaltyPercentage", + "onPremintedVoucherTransferred", + "updateOfferRoyaltyRecipients", + "updateOfferRoyaltyRecipientsBatch", + "commitToPriceDiscoveryOffer", + "sequentialCommitToOffer", + ] + ); + + 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)14256_storage": "t_struct(Range)15868_storage", + }; + + const renamedVariables = { + _isCommitable: "_isCommittable", + _royaltyPercentage: "_royaltyPercentageUnused", + }; + + context( + "Generic tests on Voucher", + getGenericContextVoucher( + deployer, + protocolDiamondAddress, + contractsAfter, + mockContracts, + voucherContractState, + preUpgradeEntitiesVoucher, + preUpgradeStorageLayout, + snapshot, + { equalCustomTypes, renamedVariables } + ) + ); + } 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("ConfigHandler", async function () { + it("setMaxRoyaltyPecentage and getMaxRoyaltyPecentage do not work anymore", async function () { + const handle = id("setMaxRoyaltyPecentage(uint16)").slice(0, 10); + const newRoyaltyPercentage = 123; + const abiCoder = new ethers.AbiCoder(); + const encdata = abiCoder.encode(["uint256"], [newRoyaltyPercentage]); + const setData = handle + encdata.slice(2); + + await expect( + deployer.sendTransaction({ to: await configHandler.getAddress(), data: setData }) + ).to.be.revertedWith("Diamond: Function does not exist"); + + const getData = id("getMaxRoyaltyPecentage").slice(0, 10); + + await expect( + deployer.sendTransaction({ to: await configHandler.getAddress(), data: getData }) + ).to.be.revertedWith("Diamond: Function does not exist"); + }); + }); + + context("Offer handler", async function () { + it("Create offer accepts fee limit", async function () { + const seller = preUpgradeEntities.sellers[0]; + const assistant = seller.wallet; + const { offer, offerDates, offerDurations } = await mockOffer({ refreshModule: true }); + + const offerFeeLimit = MaxUint256; // unlimited offer fee to not affect the tests + const disputeResolverId = preUpgradeEntities.DRs[1].id; + const agentId = "0"; + offer.royaltyInfo[0].bps = [seller.voucherInitValues.royaltyPercentage]; + + // Create the offer, test for the event + await expect( + offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId, offerFeeLimit) + ).to.emit(offerHandler, "OfferCreated"); + }); + + it("Old create offer does not work anymore", async function () { + const inputDataType = [ + "tuple(uint256,uint256,uint256,uint256,uint256,uint256,address,uint8,string,string,bool,uint256)", + "tuple(uint256,uint256,uint256,uint256)", + "tuple(uint256,uint256,uint256)", + "uint256", + "uint256", + ]; + const functionName = `createOffer(${inputDataType.join(",").replaceAll("tuple", "")})`; + const functionSelector = id(functionName).slice(0, 10); + + const seller = preUpgradeEntities.sellers[0]; + const assistant = seller.wallet; + const { offer, offerDates, offerDurations } = await mockOffer({ refreshModule: true }); + + const disputeResolverId = preUpgradeEntities.DRs[1].id; + const agentId = "0"; + + const abiCoder = new ethers.AbiCoder(); + const encdata = abiCoder.encode(inputDataType, [ + offer.toStruct().slice(0, -1), + offerDates.toStruct(), + offerDurations.toStruct(), + disputeResolverId, + agentId, + ]); + const data = functionSelector + encdata.slice(2); + + // Try to create offer with old inputs, expect revert + await expect( + assistant.sendTransaction({ to: await offerHandler.getAddress(), data: data }) + ).to.be.revertedWith("Diamond: Function does not exist"); + }); + }); + + context("Orchestration handler", async function () { + let seller, assistant, offer, offerDates, offerDurations, disputeResolverId, agentId, offerFeeLimit; + + beforeEach(async function () { + assistant = preUpgradeEntities.sellers[0].wallet; + ({ offer, offerDates, offerDurations } = await mockOffer({ refreshModule: true })); + + offerFeeLimit = MaxUint256; // unlimited offer fee to not affect the tests + disputeResolverId = preUpgradeEntities.DRs[1].id; + agentId = "0"; + }); + + context("new seller", async function () { + let emptyAuthToken, voucherInitValues; + beforeEach(async function () { + assistant = rando; + seller = mockSeller( + await assistant.getAddress(), + await assistant.getAddress(), + ZeroAddress, + await assistant.getAddress() + ); + + emptyAuthToken = mockAuthToken(); + voucherInitValues = mockVoucherInitValues(); + }); + + it("createSellerAndOffer", async function () { + // Create a seller and an offer, testing for the event + await expect( + orchestrationHandler + .connect(assistant) + .createSellerAndOffer( + seller, + offer, + offerDates, + offerDurations, + disputeResolverId, + emptyAuthToken, + voucherInitValues, + agentId, + offerFeeLimit + ) + ).to.emit(orchestrationHandler, "OfferCreated"); + }); + + it("createSellerAndOfferWithCondition", async function () { + const condition = mockCondition({ + tokenAddress: await other2.getAddress(), + tokenType: TokenType.MultiToken, + method: EvaluationMethod.Threshold, + }); + + // Create a seller and an offer with condition, testing for the events + const tx = await orchestrationHandler + .connect(assistant) + .createSellerAndOfferWithCondition( + seller, + offer, + offerDates, + offerDurations, + disputeResolverId, + condition, + emptyAuthToken, + voucherInitValues, + agentId, + offerFeeLimit + ); + + await expect(tx).to.emit(orchestrationHandler, "OfferCreated"); + }); + + it("createSellerAndOfferAndTwinWithBundle", async function () { + const [foreign20] = await deployMockTokens(["Foreign20"]); + const twin = mockTwin(await foreign20.getAddress()); + + await foreign20.connect(assistant).mint(assistant.address, 100); + await foreign20.connect(assistant).approve(await twinHandler.getAddress(), 1); // approving the twin handler + + // Create a seller, an offer with condition and a twin with bundle, testing for the events + const tx = await orchestrationHandler + .connect(assistant) + .createSellerAndOfferAndTwinWithBundle( + seller, + offer, + offerDates, + offerDurations, + disputeResolverId, + twin, + emptyAuthToken, + voucherInitValues, + agentId, + offerFeeLimit + ); + + await expect(tx).to.emit(orchestrationHandler, "OfferCreated"); + }); + + it("createSellerAndOfferWithConditionAndTwinAndBundle", async function () { + const [foreign20] = await deployMockTokens(["Foreign20"]); + const twin = mockTwin(await foreign20.getAddress()); + + await foreign20.connect(assistant).mint(assistant.address, 100); + await foreign20.connect(assistant).approve(await twinHandler.getAddress(), 1); // approving the twin handler + + const condition = mockCondition({ + tokenAddress: await other2.getAddress(), + tokenType: TokenType.MultiToken, + method: EvaluationMethod.Threshold, + }); + + // Create a seller, an offer with condition, twin and bundle + const tx = await orchestrationHandler + .connect(assistant) + .createSellerAndOfferWithConditionAndTwinAndBundle( + seller, + offer, + offerDates, + offerDurations, + disputeResolverId, + condition, + twin, + emptyAuthToken, + voucherInitValues, + agentId, + offerFeeLimit + ); + + await expect(tx).to.emit(orchestrationHandler, "OfferCreated"); + }); + }); + + context("existing seller", async function () { + let sellerMeta; + beforeEach(async function () { + sellerMeta = preUpgradeEntities.sellers[0]; + seller = sellerMeta.seller; + assistant = sellerMeta.wallet; + + offer.royaltyInfo[0].bps = [sellerMeta.voucherInitValues.royaltyPercentage]; + }); + + it("createOfferWithCondition", async function () { + const condition = mockCondition({ + tokenAddress: await other2.getAddress(), + tokenType: TokenType.MultiToken, + method: EvaluationMethod.Threshold, + }); + + // Create an offer with condition, testing for the events + const tx = await orchestrationHandler + .connect(assistant) + .createOfferWithCondition( + offer, + offerDates, + offerDurations, + disputeResolverId, + condition, + agentId, + offerFeeLimit + ); + + // OfferCreated event + await expect(tx).to.emit(orchestrationHandler, "OfferCreated"); + }); + + it("createOfferAddToGroup", async function () { + const condition = mockCondition({ + tokenType: TokenType.MultiToken, + tokenAddress: await other2.getAddress(), + method: EvaluationMethod.Threshold, + maxCommits: "3", + }); + + const nextGroupId = await groupHandler.getNextGroupId(); + + // Create an offer and add it to a group + await orchestrationHandler + .connect(assistant) + .createOfferWithCondition( + offer, + offerDates, + offerDurations, + disputeResolverId, + condition, + agentId, + offerFeeLimit + ); + + // Create an offer, add it to the existing group, testing for the events + const tx = await orchestrationHandler + .connect(assistant) + .createOfferAddToGroup( + offer, + offerDates, + offerDurations, + disputeResolverId, + nextGroupId, + agentId, + offerFeeLimit + ); + + // OfferCreated event + await expect(tx).to.emit(orchestrationHandler, "OfferCreated"); + }); + + it("createOfferAndTwinWithBundle", async function () { + const [foreign20] = await deployMockTokens(["Foreign20"]); + const twin = mockTwin(await foreign20.getAddress()); + + await foreign20.connect(assistant).mint(assistant.address, 100); + await foreign20.connect(assistant).approve(await twinHandler.getAddress(), 1); // approving the twin handler + + // Create an offer, a twin and a bundle, testing for the events + const tx = await orchestrationHandler + .connect(assistant) + .createOfferAndTwinWithBundle( + offer, + offerDates, + offerDurations, + disputeResolverId, + twin, + agentId, + offerFeeLimit + ); + + // OfferCreated event + await expect(tx).to.emit(orchestrationHandler, "OfferCreated"); + }); + + it("createOfferWithConditionAndTwinAndBundle", async function () { + const [foreign20] = await deployMockTokens(["Foreign20"]); + const twin = mockTwin(await foreign20.getAddress()); + + await foreign20.connect(assistant).mint(assistant.address, 100); + await foreign20.connect(assistant).approve(await twinHandler.getAddress(), 1); // approving the twin handler + + const condition = mockCondition({ + tokenAddress: await other2.getAddress(), + tokenType: TokenType.MultiToken, + method: EvaluationMethod.Threshold, + }); + + // Create an offer with condition, twin and bundle + const tx = await orchestrationHandler + .connect(assistant) + .createOfferWithConditionAndTwinAndBundle( + offer, + offerDates, + offerDurations, + disputeResolverId, + condition, + twin, + agentId, + offerFeeLimit + ); + + // OfferCreated event + await expect(tx).to.emit(orchestrationHandler, "OfferCreated"); + }); + }); + }); + + context("Funds handler", async function () { + context("release funds for pre-upgrade exchange works normally", async function () { + // not a breaking change, but it's a good test to see if the upgrade was successful + it("COMPLETED", async function () { + // same as expired/retracted + // seller: price + deposit, buyer: 0, protocol: fee, agent: fee + const exchangeId = 2; // redeemed already + const exchangeMeta = preUpgradeEntities.exchanges[exchangeId - 1]; + const { wallet: buyer } = preUpgradeEntities.buyers[exchangeMeta.buyerIndex]; + const { seller } = preUpgradeEntities.sellers.find((s) => s.id == exchangeMeta.sellerId); + const { offer, agentId } = preUpgradeEntities.offers.find((o) => o.offer.id == exchangeMeta.offerId); + const { agent } = preUpgradeEntities.agents.find((a) => a.id == agentId); + + const protocolFeePercent = await configHandler.getProtocolFeePercentage(); + const protocolPayoff = applyPercentage(offer.price, protocolFeePercent); + const agentPayoff = applyPercentage(offer.price, agent.feePercentage); + const sellerPayoff = + BigInt(offer.price) - BigInt(protocolPayoff) + BigInt(offer.sellerDeposit) - BigInt(agentPayoff); + // Complete the exchange, expecting event + const tx = await exchangeHandler.connect(buyer).completeExchange(exchangeId); + + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs(exchangeId, seller.id, offer.exchangeToken, sellerPayoff, buyer.address); + + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs(exchangeId, agent.id, offer.exchangeToken, agentPayoff, buyer.address); + + await expect(tx) + .to.emit(exchangeHandler, "ProtocolFeeCollected") + .withArgs(exchangeId, offer.exchangeToken, protocolPayoff, buyer.address); + }); + + it("REVOKED", async function () { + // seller: 0, buyer: price + seller deposit, protocol: 0, agent: 0 + const exchangeId = 1; // committed only + const exchangeMeta = preUpgradeEntities.exchanges[exchangeId - 1]; + const { id: buyerId } = preUpgradeEntities.buyers[exchangeMeta.buyerIndex]; + const { offer } = preUpgradeEntities.offers.find((o) => o.offer.id == exchangeMeta.offerId); + const { wallet: assistant } = preUpgradeEntities.sellers.find((s) => s.id == exchangeMeta.sellerId); + + const buyerPayoff = BigInt(offer.price) + BigInt(offer.sellerDeposit); + + // Revoke the voucher, expecting event + const tx = await exchangeHandler.connect(assistant).revokeVoucher(exchangeId); + + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs(exchangeId, buyerId, offer.exchangeToken, buyerPayoff, assistant.address); + + await expect(tx).to.not.emit(exchangeHandler, "ProtocolFeeCollected"); + }); + + it("CANCELED", async function () { + // seller: buyer cancel penalty + seller deposit, buyer: price - buyer cancel penalty, protocol: 0, agent: 0 + const exchangeId = 1; // committed only + const exchangeMeta = preUpgradeEntities.exchanges[exchangeId - 1]; + const { wallet: buyer, id: buyerId } = preUpgradeEntities.buyers[exchangeMeta.buyerIndex]; + const { seller } = preUpgradeEntities.sellers.find((s) => s.id == exchangeMeta.sellerId); + const { offer } = preUpgradeEntities.offers.find((o) => o.offer.id == exchangeMeta.offerId); + + const sellerPayoff = BigInt(offer.buyerCancelPenalty) + BigInt(offer.sellerDeposit); + const buyerPayoff = BigInt(offer.price) - BigInt(offer.buyerCancelPenalty); + + // Cancel the voucher, expecting event + const tx = await exchangeHandler.connect(buyer).cancelVoucher(exchangeId); + + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs(exchangeId, seller.id, offer.exchangeToken, sellerPayoff, buyer.address); + + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs(exchangeId, buyerId, offer.exchangeToken, buyerPayoff, buyer.address); + + await expect(tx).to.not.emit(exchangeHandler, "ProtocolFeeCollected"); + }); + + it("DECIDED", async function () { + // same as resolved + // seller: (price + deposit)*(1-buyerPercent), buyer: (price + deposit)*buyerPercent, protocol: 0, agent: 0 + const exchangeId = 5; // disputed already + const exchangeMeta = preUpgradeEntities.exchanges[exchangeId - 1]; + const { id: buyerId, wallet: buyer } = preUpgradeEntities.buyers[exchangeMeta.buyerIndex]; + const { seller } = preUpgradeEntities.sellers.find((s) => s.id == exchangeMeta.sellerId); + const { offer, disputeResolverId } = preUpgradeEntities.offers.find( + (o) => o.offer.id == exchangeMeta.offerId + ); + const { wallet: assistantDR } = preUpgradeEntities.DRs.find((d) => d.id == disputeResolverId); + + const buyerPercent = 1234; + const pot = BigInt(offer.price) + BigInt(offer.sellerDeposit); + const buyerPayoff = applyPercentage(pot, buyerPercent); + const sellerPayoff = pot - BigInt(buyerPayoff); + + // escalate dispute, so DR can resolve it + await disputeHandler.connect(buyer).escalateDispute(exchangeId); + + // Decide the exchange, expecting event + const tx = await disputeHandler.connect(assistantDR).decideDispute(exchangeId, buyerPercent); + + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs(exchangeId, seller.id, offer.exchangeToken, sellerPayoff, assistantDR.address); + + await expect(tx) + .to.emit(exchangeHandler, "FundsReleased") + .withArgs(exchangeId, buyerId, offer.exchangeToken, buyerPayoff, assistantDR.address); + + await expect(tx).to.not.emit(exchangeHandler, "ProtocolFeeCollected"); + }); + }); + }); + + 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 isAllowed = await metaTransactionsHandler["isFunctionAllowlisted(bytes32)"](hash); + expect(isAllowed).to.be.false; + } + }); + + it("Function hashes from from addedFunctionsHashes list should be allowlisted", async function () { + for (const hash of addedFunctionHashes) { + const isAllowed = await metaTransactionsHandler["isFunctionAllowlisted(bytes32)"](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("New methods", async function () { + context("Config handler facet", async function () { + it("setMaxRoyaltyPercentage replaces setMaxRoyaltyPecentage)", async function () { + const newRoyaltyPercentage = 123; + + await configHandler.setMaxRoyaltyPercentage(newRoyaltyPercentage); + + const royaltyPercentage = await configHandler.getMaxRoyaltyPercentage(); + + expect(royaltyPercentage).to.equal(newRoyaltyPercentage); + }); + }); + + context("Exchange handler facet", async function () { + it("getEIP2981Royalties", async function () { + const sellers = preUpgradeEntities.sellers; + for (const offer of preUpgradeEntities.offers) { + const seller = sellers.find((s) => s.id == offer.offer.sellerId); + + const [returnedReceiver, returnedRoyaltyPercentage] = await exchangeHandler.getEIP2981Royalties( + offer.offer.id, + false + ); + + expect(returnedReceiver).to.equal(seller.wallet.address, `Receiver for offer ${offer.id} is not correct`); + expect(returnedRoyaltyPercentage).to.equal( + offer.royaltyInfo[0].bps[0], + `Percentage for offer ${offer.id} is not correct` + ); + expect(returnedRoyaltyPercentage).to.equal( + seller.voucherInitValues.royaltyPercentage, + `Percentage for offer ${offer.id} is not correct` + ); + } + + for (const exchange of preUpgradeEntities.exchanges) { + const seller = sellers.find((s) => s.id == exchange.sellerId); + const [returnedReceiver, returnedRoyaltyPercentage] = await exchangeHandler.getEIP2981Royalties( + exchange.exchangeId, + true + ); + expect(returnedReceiver).to.equal( + seller.wallet.address, + `Receiver for exchange ${exchange.exchangeId} is not correct` + ); + expect(returnedRoyaltyPercentage).to.equal( + seller.voucherInitValues.royaltyPercentage, + `Percentage for exchange ${exchange.exchangeId} is not correct` + ); + } + }); + + it("getRoyalties", async function () { + const sellers = preUpgradeEntities.sellers; + for (const offer of preUpgradeEntities.offers) { + const seller = sellers.find((s) => s.id == offer.offer.sellerId); + + const queryId = deriveTokenId(offer.offer.id, "999"); // some exchange id that does not exist. Simulates the preminted offer + const [returnedReceiver, returnedRoyaltyPercentage] = await exchangeHandler.getRoyalties(queryId); + + expect(returnedReceiver).to.deep.equal( + [seller.wallet.address], + `Receiver for offer ${offer.offer.id} is not correct` + ); + expect(returnedRoyaltyPercentage).to.deep.equal( + offer.royaltyInfo[0].bps, + `Percentage for offer ${offer.id} is not correct` + ); + expect(returnedRoyaltyPercentage).to.deep.equal( + [seller.voucherInitValues.royaltyPercentage], + `Percentage for offer ${offer.id} is not correct` + ); + } + + for (const exchange of preUpgradeEntities.exchanges) { + const seller = sellers.find((s) => s.id == exchange.sellerId); + const queryId = exchange.exchangeId; + + // test with token id + const [returnedReceiver, returnedRoyaltyPercentage] = await exchangeHandler.getRoyalties(queryId); + expect(returnedReceiver).to.deep.equal( + [seller.wallet.address], + `Receiver for exchange ${exchange.exchangeId} is not correct` + ); + expect(returnedRoyaltyPercentage).to.deep.equal( + [seller.voucherInitValues.royaltyPercentage], + `Percentage for exchange ${exchange.exchangeId} is not correct` + ); + } + }); + }); + + context("Seller handler facet", async function () { + let admin, sellerId, sellerRoyalties; + let royaltyRecipientInfoList; + + context("Royalty recipients", async function () { + beforeEach(async function () { + const seller = preUpgradeEntities.sellers[0]; + admin = seller.wallet; + sellerId = seller.id; + sellerRoyalties = seller.voucherInitValues.royaltyPercentage; + + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "100"), + new RoyaltyRecipientInfo(other2.address, "200"), + new RoyaltyRecipientInfo(other3.address, "300"), + ]); + }); + + it("Add royalty recipients", async function () { + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, sellerRoyalties), + ...royaltyRecipientInfoList.royaltyRecipientInfos, + ]); + + // Add royalty recipients + const tx = await accountHandler + .connect(admin) + .addRoyaltyRecipients(sellerId, royaltyRecipientInfoList.toStruct()); + + const event = getEvent(await tx.wait(), accountHandler, "RoyaltyRecipientsChanged"); + + const returnedRecipientList = RoyaltyRecipientInfoList.fromStruct(event.royaltyRecipients); + + expect(event.sellerId).to.equal(sellerId); + expect(event.executedBy).to.equal(admin.address); + expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientInfoList); + }); + + it("Update royalty recipients", async function () { + // Add royalty recipients + await accountHandler.connect(admin).addRoyaltyRecipients(sellerId, royaltyRecipientInfoList.toStruct()); + + // update data + const royaltyRecipientInfoIds = [1, 0, 3]; + const royaltyRecipientInfoListUpdates = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other4.address, "400"), // change address and percentage, keep name + new RoyaltyRecipientInfo(ZeroAddress, "150"), // change percentage of default recipient + new RoyaltyRecipientInfo(other3.address, "300"), // change nothing + ]); + + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, "150"), + new RoyaltyRecipientInfo(other4.address, "400"), + new RoyaltyRecipientInfo(other2.address, "200"), + new RoyaltyRecipientInfo(other3.address, "300"), + ]); + + // Update royalty recipients + const tx = await accountHandler + .connect(admin) + .updateRoyaltyRecipients(sellerId, royaltyRecipientInfoIds, royaltyRecipientInfoListUpdates.toStruct()); + + const event = getEvent(await tx.wait(), accountHandler, "RoyaltyRecipientsChanged"); + + const returnedRecipientList = RoyaltyRecipientInfoList.fromStruct(event.royaltyRecipients); + + expect(event.sellerId).to.equal(sellerId); + expect(event.executedBy).to.equal(admin.address); + expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientInfoList); + }); + + it("Remove royalty recipients", async function () { + royaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + ...royaltyRecipientInfoList.royaltyRecipientInfos, + new RoyaltyRecipientInfo(other4.address, "400"), + new RoyaltyRecipientInfo(other5.address, "500"), + new RoyaltyRecipientInfo(other6.address, "600"), + ]); + // add first set of royalty recipients + await accountHandler.connect(admin).addRoyaltyRecipients(sellerId, royaltyRecipientInfoList.toStruct()); + + // ids to remove + const royaltyRecipientInfoIds = [1, 3, 4, 6]; + + // Removal process: [0,1,2,3,4,5,6]->[0,1,2,3,4,5]->[0,1,2,3,5]->[0,1,2,5]->[0,5,2] + const expectedRoyaltyRecipientInfoList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(ZeroAddress, sellerRoyalties), // default + royaltyRecipientInfoList.royaltyRecipientInfos[4], + royaltyRecipientInfoList.royaltyRecipientInfos[1], + ]); + + // Remove royalty recipients + const tx = await accountHandler.connect(admin).removeRoyaltyRecipients(sellerId, royaltyRecipientInfoIds); + + const event = getEvent(await tx.wait(), accountHandler, "RoyaltyRecipientsChanged"); + + const returnedRecipientList = RoyaltyRecipientInfoList.fromStruct(event.royaltyRecipients); + + expect(event.sellerId).to.equal(sellerId); + expect(event.executedBy).to.equal(admin.address); + expect(returnedRecipientList).to.deep.equal(expectedRoyaltyRecipientInfoList); + }); + }); + + it("getSellersCollectionsPaginated()", async function () { + const seller = preUpgradeEntities.sellers[0]; + const expectedDefaultAddress = seller.voucherContractAddress; + const voucherInitValues = seller.voucherInitValues; + const beaconProxyAddress = await configHandler.getBeaconProxyAddress(); + + const additionalCollections = new CollectionList([]); + for (let i = 1; i <= 5; i++) { + const externalId = `Brand${i}`; + voucherInitValues.collectionSalt = encodeBytes32String(externalId); + const expectedCollectionAddress = calculateCloneAddress( + await accountHandler.getAddress(), + beaconProxyAddress, + seller.wallet.address, + voucherInitValues.collectionSalt + ); + voucherInitValues.contractURI = `https://brand${i}.com`; + + // Create a new collection + await accountHandler.connect(seller.wallet).createNewCollection(externalId, voucherInitValues); + + // Add to expected collections + additionalCollections.collections.push(new Collection(expectedCollectionAddress, externalId)); + } + + const limit = 3; + const offset = 1; + + const expectedCollections = new CollectionList( + additionalCollections.collections.slice(offset, offset + limit) + ); + + const [defaultVoucherAddress, collections] = await accountHandler + .connect(rando) + .getSellersCollectionsPaginated(seller.id, limit, offset); + const returnedCollections = CollectionList.fromStruct(collections); + + expect(defaultVoucherAddress).to.equal(expectedDefaultAddress, "Wrong default voucher address"); + expect(returnedCollections).to.deep.equal(expectedCollections, "Wrong additional collections"); + }); + }); + + context("Offer handler facet", async function () { + let seller, admin, assistant; + let newRoyaltyInfo, expectedRoyaltyInfo; + + context("Royalty recipients", async function () { + beforeEach(async function () { + seller = preUpgradeEntities.sellers[0]; + assistant = admin = seller.wallet; + + // Register royalty recipients + const royaltyRecipientList = new RoyaltyRecipientInfoList([ + new RoyaltyRecipientInfo(other1.address, "50"), + new RoyaltyRecipientInfo(other2.address, "50"), + new RoyaltyRecipientInfo(rando.address, "50"), + ]); + + await accountHandler.connect(admin).addRoyaltyRecipients(seller.id, royaltyRecipientList.toStruct()); + + const recipients = [other1.address, other2.address, ZeroAddress, rando.address]; + const bps = ["100", "150", "500", "200"]; + newRoyaltyInfo = new RoyaltyInfo(recipients, bps); + + const expectedRecipients = [...recipients]; + expectedRecipients[2] = seller.wallet.address; + expectedRoyaltyInfo = new RoyaltyInfo(recipients, bps).toStruct(); + }); + + it("Update offer royalty recipients", async function () { + const offerId = seller.offerIds[0]; + + // Update the royalty recipients, testing for the event + await expect(offerHandler.connect(assistant).updateOfferRoyaltyRecipients(offerId, newRoyaltyInfo)) + .to.emit(offerHandler, "OfferRoyaltyInfoUpdated") + .withArgs(offerId, seller.id, compareRoyaltyInfo.bind(expectedRoyaltyInfo), assistant.address); + }); + + it("Update offer royalty recipients batch", async function () { + const offersToUpdate = seller.offerIds; + + // Update the royalty info, testing for the event + const tx = await offerHandler + .connect(assistant) + .updateOfferRoyaltyRecipientsBatch(offersToUpdate, newRoyaltyInfo); + await expect(tx) + .to.emit(offerHandler, "OfferRoyaltyInfoUpdated") + .withArgs(offersToUpdate[0], seller.id, compareRoyaltyInfo.bind(expectedRoyaltyInfo), assistant.address); + + await expect(tx) + .to.emit(offerHandler, "OfferRoyaltyInfoUpdated") + .withArgs(offersToUpdate[1], seller.id, compareRoyaltyInfo.bind(expectedRoyaltyInfo), assistant.address); + + await expect(tx) + .to.emit(offerHandler, "OfferRoyaltyInfoUpdated") + .withArgs(offersToUpdate[2], seller.id, compareRoyaltyInfo.bind(expectedRoyaltyInfo), assistant.address); + }); + }); + }); + + context("Price discovery handler facet", async function () { + it("Commit to price discovery offer", async function () { + // Deploy PriceDiscovery contract + const PriceDiscoveryFactory = await getContractFactory("PriceDiscoveryMock"); + const priceDiscoveryContract = await PriceDiscoveryFactory.deploy(); + await priceDiscoveryContract.waitForDeployment(); + + const seller = preUpgradeEntities.sellers[0]; + const assistant = seller.wallet; + + const mo = await mockOffer({ refreshModule: true }); + const { offer, offerDates, offerDurations } = mo; + offer.id = await offerHandler.getNextOfferId(); + offer.priceType = PriceType.Discovery; + offer.price = "0"; + offer.buyerCancelPenalty = "0"; + offer.royaltyInfo[0].bps = [seller.voucherInitValues.royaltyPercentage]; + const offerFeeLimit = MaxUint256; // unlimited offer fee to not affect the tests + const disputeResolverId = preUpgradeEntities.DRs[1].id; + const agentId = "0"; + + // Mock exchange + const exchange = mockExchange({ + id: await exchangeHandler.getNextExchangeId(), + offerId: offer.id, + finalizedDate: "0", + }); + + // Create the offer, reserve range and premint vouchers + await offerHandler + .connect(assistant) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId, offerFeeLimit); + await offerHandler.connect(assistant).reserveRange(offer.id, offer.quantityAvailable, assistant.address); + const bosonVoucher = await getContractAt("BosonVoucher", seller.voucherContractAddress); + await bosonVoucher.connect(assistant).preMint(offer.id, offer.quantityAvailable); + + // Deposit seller funds so the commit will succeed + const sellerPool = offer.sellerDeposit; + await fundsHandler.connect(assistant).depositFunds(seller.id, ZeroAddress, sellerPool, { value: sellerPool }); + + // Price on secondary market + const price = 100n; + const tokenId = deriveTokenId(offer.id, exchange.id); + + // Prepare calldata for PriceDiscovery contract + const order = { + seller: assistant.address, + buyer: buyer.address, + voucherContract: seller.voucherContractAddress, + tokenId: tokenId, + exchangeToken: await weth.getAddress(), // when offer is in native, we need to use wrapped native + price: price, + }; + + const priceDiscoveryData = priceDiscoveryContract.interface.encodeFunctionData("fulfilBuyOrder", [order]); + const priceDiscoveryContractAddress = await priceDiscoveryContract.getAddress(); + + const priceDiscovery = new PriceDiscovery( + price, + Side.Ask, + priceDiscoveryContractAddress, + priceDiscoveryContractAddress, + priceDiscoveryData + ); + + // Approve transfers + // Buyer needs to approve the protocol to transfer the ETH + await weth.connect(buyer).deposit({ value: price }); + await weth.connect(buyer).approve(await priceDiscoveryHandler.getAddress(), price); + + // Seller approves price discovery to transfer the voucher + await bosonVoucher.connect(assistant).setApprovalForAll(await priceDiscoveryContract.getAddress(), true); + + // Seller also approves the protocol to encumber the paid price + await weth.connect(assistant).approve(await priceDiscoveryHandler.getAddress(), price); + + const newBuyer = mockBuyer(buyer.address); + newBuyer.id = await accountHandler.getNextAccountId(); + exchange.buyerId = newBuyer.id; + + const tx = await priceDiscoveryHandler + .connect(buyer) + .commitToPriceDiscoveryOffer(buyer.address, tokenId, priceDiscovery); + + // Get the block timestamp of the confirmed tx + const block = await provider.getBlock(tx.blockNumber); + + // Update the committed date in the expected exchange struct with the block timestamp of the tx + const voucher = mockVoucher({ + committedDate: block.timestamp.toString(), + validUntilDate: calculateVoucherExpiry( + block, + offerDates.voucherRedeemableFrom, + offerDurations.voucherValid + ), + redeemedDate: "0", + }); + + await expect(tx) + .to.emit(priceDiscoveryHandler, "BuyerCommitted") + .withArgs( + offer.id, + newBuyer.id, + exchange.id, + exchange.toStruct(), + voucher.toStruct(), + seller.voucherContractAddress + ); + }); + }); + + context("Sequential commit handler facet", async function () { + it("Sequential Commit to offer", async function () { + // Deploy PriceDiscovery contract + const PriceDiscoveryFactory = await getContractFactory("PriceDiscoveryMock"); + const priceDiscoveryContract = await PriceDiscoveryFactory.deploy(); + await priceDiscoveryContract.waitForDeployment(); + + // Create buyer with price discovery client address to not mess up ids in tests + const priceDiscoveryClientAddress = await configHandler.getPriceDiscoveryAddress(); + await accountHandler.createBuyer(mockBuyer(priceDiscoveryClientAddress)); + + const exchangeMeta = preUpgradeEntities.exchanges[0]; + const { offer } = preUpgradeEntities.offers[exchangeMeta.offerId - 1]; + const seller = preUpgradeEntities.sellers.find((s) => s.id == exchangeMeta.sellerId); + + const exchange = mockExchange({ + id: exchangeMeta.exchangeId, + offerId: offer.id, + finalizedDate: "0", + }); + + const bosonVoucher = await getContractAt("BosonVoucher", seller.voucherContractAddress); + + const price = offer.price; + const price2 = (BigInt(price) * 11n) / 10n; // 10% above the original price + const tokenId = deriveTokenId(exchangeMeta.offerId, exchangeMeta.exchangeId); + + const reseller = preUpgradeEntities.buyers[exchangeMeta.buyerIndex].wallet; + + // Prepare calldata for PriceDiscovery contract + const order = { + seller: reseller.address, + buyer: buyer.address, + voucherContract: seller.voucherContractAddress, + tokenId: tokenId, + exchangeToken: offer.exchangeToken == ZeroAddress ? await weth.getAddress() : offer.exchangeToken, // when offer is in native, we need to use wrapped native + price: price2, + }; + + const priceDiscoveryData = priceDiscoveryContract.interface.encodeFunctionData("fulfilBuyOrder", [order]); + const priceDiscoveryContractAddress = await priceDiscoveryContract.getAddress(); + + const priceDiscovery = new PriceDiscovery( + price2, + Side.Ask, + priceDiscoveryContractAddress, + priceDiscoveryContractAddress, + priceDiscoveryData + ); + + const exchangeToken = await getContractAt( + "@openzeppelin/contracts/token/ERC20/IERC20.sol:IERC20", + order.exchangeToken + ); + + // Approve transfers + // Buyer needs to approve the protocol to transfer the exchange token + if (offer.exchangeToken == ZeroAddress) { + await weth.connect(buyer).deposit({ value: price2 }); + } else { + await exchangeToken.connect(buyer).mint(buyer.address, price2); + } + await exchangeToken.connect(buyer).approve(await sequentialCommitHandler.getAddress(), price2); + + // Seller approves price discovery to transfer the voucher + await bosonVoucher.connect(reseller).setApprovalForAll(await priceDiscoveryContract.getAddress(), true); + + // Seller also approves the protocol to encumber the paid price + await exchangeToken.connect(reseller).approve(await sequentialCommitHandler.getAddress(), price2); + + const newBuyer = mockBuyer(buyer.address); + newBuyer.id = await accountHandler.getNextAccountId(); + exchange.buyerId = newBuyer.id; + + // Get voucher info before the approval. Sequential commit should not change it + const [, , returnedVoucher] = await exchangeHandler.getExchange(exchange.id); + const voucher = Voucher.fromStruct(returnedVoucher); + + // Sequential commit to offer, retrieving the event + const tx = await sequentialCommitHandler + .connect(buyer) + .sequentialCommitToOffer(buyer.address, tokenId, priceDiscovery); + + await expect(tx) + .to.emit(sequentialCommitHandler, "BuyerCommitted") + .withArgs( + exchange.offerId, + newBuyer.id, + exchange.id, + exchange.toStruct(), + voucher.toStruct(), + buyer.address + ); + }); + }); + + context("Boson voucher", async function () { + it("royalty info", async function () { + for (const exchange of preUpgradeEntities.exchanges) { + const seller = preUpgradeEntities.sellers.find((s) => s.id == exchange.sellerId); + const bosonVoucher = await getContractAt("BosonVoucher", seller.voucherContractAddress); + + const [, state] = await exchangeHandler.getExchangeState(exchange.exchangeId); + + const tokenId = deriveTokenId(exchange.offerId, exchange.exchangeId); + const offerPrice = parseUnits("1", "ether"); + const [receiver, royaltyAmount] = await bosonVoucher.royaltyInfo(tokenId, offerPrice); + + let expectedReceiver, expectedRoyaltyAmount; + if (state > 0n) { + // voucher was burned + expectedReceiver = ZeroAddress; + expectedRoyaltyAmount = 0n; + } else { + expectedReceiver = seller.wallet.address; + expectedRoyaltyAmount = applyPercentage(offerPrice, seller.voucherInitValues.royaltyPercentage); + } + + expect(receiver).to.equal(expectedReceiver, `Receiver for exchange ${exchange.exchangeId} is not correct`); + expect(royaltyAmount).to.equal( + expectedRoyaltyAmount, + `Royalty for exchange ${exchange.exchangeId} is not correct` + ); + } + }); + }); + + context("Price discovery client", async function () { + it("Can receive voucher only during price discovery", async function () { + const tokenId = 1; + const [foreign721] = await deployMockTokens(["Foreign721"]); + await foreign721.connect(rando).mint(tokenId, 1); + + const bosonPriceDiscovery = await getContractAt( + "BosonPriceDiscovery", + await configHandler.getPriceDiscoveryAddress() + ); + const bosonErrors = await getContractAt("BosonErrors", await bosonPriceDiscovery.getAddress()); + + await expect( + foreign721 + .connect(rando) + ["safeTransferFrom(address,address,uint256)"]( + rando.address, + await bosonPriceDiscovery.getAddress(), + tokenId + ) + ).to.revertedWithCustomError(bosonErrors, RevertReasons.UNEXPECTED_ERC721_RECEIVED); + }); + }); + }); + + context("Bug fixes", async function () { + context("Funds handler facet", async function () { + it("FundsEncumbered event is emitted during escalate dispute", async function () { + const exchangeId = 5; // exchange with dispute + const exchange = preUpgradeEntities.exchanges[exchangeId - 1]; + const buyer = preUpgradeEntities.buyers[exchange.buyerIndex].wallet; + + let buyerEscalationDeposit = 0; // Currently, DRfees can only be 0 + + await expect( + disputeHandler.connect(buyer).escalateDispute(exchangeId, { value: buyerEscalationDeposit }) + ).to.emit(disputeHandler, "FundsEncumbered"); + }); + }); + }); + }); +}); diff --git a/test/upgrade/clients/01_generic.js b/test/upgrade/clients/01_generic.js index 6c10a4016..477871d43 100644 --- a/test/upgrade/clients/01_generic.js +++ b/test/upgrade/clients/01_generic.js @@ -18,7 +18,7 @@ function getGenericContext( preUpgradeEntities, preUpgradeStorageLayout, snapshot, - equalCustomTypes + { equalCustomTypes, renamedVariables } ) { const genericContextFunction = async function () { afterEach(async function () { @@ -44,7 +44,7 @@ function getGenericContext( const postUpgradeStorageLayout = await getStorageLayout("BosonVoucher"); assert( - compareStorageLayouts(preUpgradeStorageLayout, postUpgradeStorageLayout, equalCustomTypes), + compareStorageLayouts(preUpgradeStorageLayout, postUpgradeStorageLayout, equalCustomTypes, renamedVariables), "Upgrade breaks storage layout" ); }); diff --git a/test/util/mock.js b/test/util/mock.js index 83ac93a18..e536d622e 100644 --- a/test/util/mock.js +++ b/test/util/mock.js @@ -58,7 +58,7 @@ async function mockOfferDates() { } // Returns a mock offer with price in native token -async function mockOffer({ refreshModule } = {}) { +async function mockOffer({ refreshModule, legacyOffer } = {}) { if (refreshModule) { decache("../../scripts/domain/Offer.js"); Offer = require("../../scripts/domain/Offer.js"); @@ -80,21 +80,38 @@ async function mockOffer({ refreshModule } = {}) { const royaltyInfo = [new RoyaltyInfo([ZeroAddress], ["0"])]; // Create a valid offer, then set fields in tests directly - let offer = new Offer( - id, - sellerId, - price, - sellerDeposit, - buyerCancelPenalty, - quantityAvailable, - exchangeToken, - priceType, - metadataUri, - metadataHash, - voided, - collectionIndex, - royaltyInfo - ); + let offer; + if (legacyOffer) { + offer = new Offer( + id, + sellerId, + price, + sellerDeposit, + buyerCancelPenalty, + quantityAvailable, + exchangeToken, + metadataUri, + metadataHash, + voided, + collectionIndex + ); + } else { + offer = new Offer( + id, + sellerId, + price, + sellerDeposit, + buyerCancelPenalty, + quantityAvailable, + exchangeToken, + priceType, + metadataUri, + metadataHash, + voided, + collectionIndex, + royaltyInfo + ); + } const offerDates = await mockOfferDates(); const offerDurations = mockOfferDurations(); diff --git a/test/util/upgrade.js b/test/util/upgrade.js index dedaa1d7e..50cbab927 100644 --- a/test/util/upgrade.js +++ b/test/util/upgrade.js @@ -2,6 +2,7 @@ const shell = require("shelljs"); const _ = require("lodash"); const { getStorageAt } = require("@nomicfoundation/hardhat-network-helpers"); const hre = require("hardhat"); +const { expect } = require("chai"); const decache = require("decache"); const { id: ethersId, @@ -38,8 +39,8 @@ const { mockCondition, mockTwin, } = require("./mock"); -const { setNextBlockTimestamp, paddingType, getMappingStoragePosition } = require("./utils.js"); -const { oneMonth, oneDay } = require("./constants"); +const { setNextBlockTimestamp, paddingType, getMappingStoragePosition, deriveTokenId } = require("./utils.js"); +const { oneMonth, oneDay, oneWeek } = require("./constants"); const { getInterfaceIds } = require("../../scripts/config/supported-interfaces.js"); const { deployMockTokens } = require("../../scripts/util/deploy-mock-tokens"); const { readContracts } = require("../../scripts/util/utils"); @@ -60,6 +61,7 @@ let Condition = require("../../scripts/domain/Condition"); // Common vars const versionsWithActivateDRFunction = ["v2.0.0", "v2.1.0"]; 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 +const versionsBelowV2_4 = [...versionsBelowV2_3, "v2.3.0"]; // different offer struct, different createOffer function let rando; let preUpgradeInterfaceIds, preUpgradeVersions; let facets, versionTags; @@ -120,6 +122,12 @@ async function deploySuite(deployer, newVersion) { throw new Error(`No deploy config found for tag ${tag}`); } + // versions up to v2.3. have typo in the deploy config, so we need to mimic it here + if (isOldOZVersion || tag.startsWith("v2.3")) { + deployConfig["ConfigHandlerFacet"].init[1]["maxRoyaltyPecentage"] = + deployConfig["ConfigHandlerFacet"].init[1]["maxRoyaltyPercentage"]; + } + await hre.run("compile"); // run deploy suite, which automatically compiles the contracts await hre.run("deploy-suite", { @@ -334,6 +342,7 @@ async function populateProtocolContract( let sellers = []; let buyers = []; let agents = []; + let royaltyRecipients = []; let offers = []; let groups = []; let twins = []; @@ -346,6 +355,7 @@ async function populateProtocolContract( DR: 1, AGENT: 2, BUYER: 3, + ROYALTY_RECIPIENT: 4, }; const entities = [ @@ -364,6 +374,9 @@ async function populateProtocolContract( entityType.BUYER, entityType.BUYER, entityType.BUYER, + entityType.ROYALTY_RECIPIENT, // For next upgrade, it might make sense to move royalty recipients right after corresponding sellers to ensure correct account ids in tests + entityType.ROYALTY_RECIPIENT, + entityType.ROYALTY_RECIPIENT, ]; let nextAccountId = Number(await accountHandler.getNextAccountId()); @@ -487,6 +500,12 @@ async function populateProtocolContract( break; } + case entityType.ROYALTY_RECIPIENT: { + // Just a placeholder for now + const id = nextAccountId.toString(); + royaltyRecipients.push({ wallet: connectedWallet, id: id }); + break; + } } nextAccountId++; @@ -505,7 +524,11 @@ async function populateProtocolContract( for (let i = 0; i < sellers.length; i++) { for (let j = i; j >= 0; j--) { // Mock offer, offerDates and offerDurations - const { offer, offerDates, offerDurations } = await mockOffer(); + const offerStructV2_3 = versionsBelowV2_4.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion); + const { offer, offerDates, offerDurations } = await mockOffer({ + refreshModule: true, + legacyOffer: offerStructV2_3, + }); // Set unique offer properties based on offer id offer.id = `${offerId}`; @@ -528,19 +551,33 @@ async function populateProtocolContract( // Set unique offerDurations based on offer id offerDurations.disputePeriod = `${(offerId + 1) * Number(oneMonth)}`; offerDurations.voucherValid = `${(offerId + 1) * Number(oneMonth)}`; - offerDurations.resolutionPeriod = `${(offerId + 1) * Number(oneDay)}`; + offerDurations.resolutionPeriod = `${(offerId + 1) * Number(oneDay) + Number(oneWeek)}`; // choose one DR and agent const disputeResolverId = DRs[offerId % 3].disputeResolver.id; const agentId = agents[offerId % 2].agent.id; const offerFeeLimit = MaxUint256; // unlimited offer fee to not affect the tests + const royaltyInfo = [ + { + bps: [`${sellers[j].voucherInitValues.royaltyPercentage}`], + recipients: [ZeroAddress], + }, + ]; + // create an offer - await offerHandler - .connect(sellers[j].wallet) - .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId, offerFeeLimit); + if (offerStructV2_3) { + await offerHandler + .connect(sellers[j].wallet) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + } else { + offer.royaltyInfo = royaltyInfo; + await offerHandler + .connect(sellers[j].wallet) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId, offerFeeLimit); + } - offers.push({ offer, offerDates, offerDurations, disputeResolverId, agentId }); + offers.push({ offer, offerDates, offerDurations, disputeResolverId, agentId, royaltyInfo }); sellers[j].offerIds.push(offerId); // Deposit seller funds so the commit will succeed @@ -700,7 +737,7 @@ async function populateProtocolContract( .commitToOffer(await buyerWallet.getAddress(), offer.id, { value: msgValue }); } - exchanges.push({ exchangeId: exchangeId, offerId: offer.id, buyerIndex: j }); + exchanges.push({ exchangeId: exchangeId, offerId: offer.id, buyerIndex: j, sellerId: offer.sellerId }); exchangeId++; } } @@ -710,11 +747,11 @@ async function populateProtocolContract( const exchange = exchanges[id - 1]; // If exchange has twins, mint them so the transfer can succeed - // const offer = offers[Number(exchange.offerId) - 1]; const offer = offers.find((o) => o.offer.id == exchange.offerId); const seller = sellers.find((s) => s.seller.id == offer.offer.sellerId); - if (twinHandler && Number(seller.id) % 2 == 1) { + if (twinHandler) { const bundle = bundles.find((b) => b.sellerId == seller.id); + if (!bundle) continue; // no twins for this seller const twinsIds = bundle.twinIds; for (const twinId of twinsIds) { const [, twin] = await twinHandler.getTwin(twinId); @@ -759,7 +796,7 @@ async function populateProtocolContract( 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, bosonVouchers }; + return { DRs, sellers, buyers, agents, offers, exchanges, bundles, groups, twins, bosonVouchers, royaltyRecipients }; } // Returns protocol state for provided entities @@ -777,8 +814,8 @@ async function getProtocolContractState( configHandler, }, { mockToken, mockTwinTokens }, - { DRs, sellers, buyers, agents, offers, exchanges, bundles, groups, twins }, - isBefore = false + { DRs, sellers, buyers, agents, offers, exchanges, bundles, groups, twins, royaltyRecipients }, + { isBefore, skipFacets } = { isBefore: false, skipFacets: [] } ) { rando = (await getSigners())[10]; // random account making the calls @@ -796,6 +833,7 @@ async function getProtocolContractState( metaTxPrivateContractState, protocolStatusPrivateContractState, protocolLookupsPrivateContractState, + protocolEntitiesPrivateContractState, ] = await Promise.all([ getAccountContractState(accountHandler, { DRs, sellers, buyers, agents }, isBefore), getOfferContractState(offerHandler, offers), @@ -807,14 +845,15 @@ async function getProtocolContractState( getGroupContractState(groupHandler, groups), getTwinContractState(twinHandler, twins), getMetaTxContractState(), - getMetaTxPrivateContractState(protocolDiamondAddress), - getProtocolStatusPrivateContractState(protocolDiamondAddress), + getMetaTxPrivateContractState(protocolDiamondAddress, skipFacets), + getProtocolStatusPrivateContractState(protocolDiamondAddress, skipFacets), getProtocolLookupsPrivateContractState( protocolDiamondAddress, { mockToken, mockTwinTokens }, - { sellers, DRs, agents, buyers, offers, groups, twins }, + { sellers, DRs, agents, buyers, offers, groups, twins, royaltyRecipients }, groupHandler ), + getProtocolEntitiesPrivateContractState(protocolDiamondAddress, { exchanges, royaltyRecipients }), ]); return { @@ -831,6 +870,7 @@ async function getProtocolContractState( metaTxPrivateContractState, protocolStatusPrivateContractState, protocolLookupsPrivateContractState, + protocolEntitiesPrivateContractState, }; } @@ -996,6 +1036,7 @@ async function getBundleContractState(bundleHandler, bundles) { async function getConfigContractState(configHandler, isBefore = false) { const isBefore2_3_0 = versionsBelowV2_3.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion); + const isBefore2_4_0 = versionsBelowV2_4.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion); const configHandlerRando = configHandler.connect(rando); const [ tokenAddress, @@ -1049,7 +1090,7 @@ async function getConfigContractState(configHandler, isBefore = false) { configHandlerRando.getAuthTokenContract(AuthTokenType.Lens), configHandlerRando.getAuthTokenContract(AuthTokenType.ENS), isBefore2_3_0 ? configHandlerRando.getMaxExchangesPerBatch() : Promise.resolve(0n), - configHandlerRando.getMaxRoyaltyPercentage(), + isBefore2_4_0 ? configHandlerRando.getMaxRoyaltyPecentage() : configHandlerRando.getMaxRoyaltyPercentage(), configHandlerRando.getMaxResolutionPeriod(), configHandlerRando.getMinDisputePeriod(), configHandlerRando.getAccessControllerAddress(), @@ -1150,7 +1191,7 @@ async function getMetaTxContractState() { return {}; } -async function getMetaTxPrivateContractState(protocolDiamondAddress) { +async function getMetaTxPrivateContractState(protocolDiamondAddress, skipFacets = []) { /* ProtocolMetaTxInfo storage layout @@ -1235,7 +1276,9 @@ async function getMetaTxPrivateContractState(protocolDiamondAddress) { "MetaTransactionsHandlerFacet", "OrchestrationHandlerFacet1", "OrchestrationHandlerFacet2", - ]; + "PriceDiscoveryHandlerFacet", + "SequentialCommitHandlerFacet", + ].filter((id) => !skipFacets.includes(id)); const selectors = await getMetaTransactionsHandlerFacetInitArgs(facets); @@ -1247,7 +1290,7 @@ async function getMetaTxPrivateContractState(protocolDiamondAddress) { return { inTransactionInfo, domainSeparator, cachedChainId, inputTypesState, hashInfoState, isAllowlistedState }; } -async function getProtocolStatusPrivateContractState(protocolDiamondAddress) { +async function getProtocolStatusPrivateContractState(protocolDiamondAddress, ignoreInterfaceIds = []) { /* ProtocolStatus storage layout @@ -1256,6 +1299,8 @@ async function getProtocolStatusPrivateContractState(protocolDiamondAddress) { #2 [ ] // placeholder for initializedInterfaces #3 [ ] // placeholder for initializedVersions #4 [ version ] - not here as should be updated one very upgrade + #5 [ incomingVoucherId ] // should always be empty + #6 [ incomingVoucherCloneAddress ] // should always be empty */ // starting slot @@ -1272,9 +1317,14 @@ async function getProtocolStatusPrivateContractState(protocolDiamondAddress) { // initializedInterfaces if (!preUpgradeInterfaceIds) { // Only interfaces registered before upgrade are relevant for tests, so we load them only once - preUpgradeInterfaceIds = await getInterfaceIds(); + preUpgradeInterfaceIds = await getInterfaceIds(false); + + ignoreInterfaceIds.forEach((id) => { + delete preUpgradeInterfaceIds[id]; + }); } + // initializedInterfaces const initializedInterfacesState = []; for (const interfaceId of Object.values(preUpgradeInterfaceIds)) { const storageSlot = getMappingStoragePosition(protocolStatusStorageSlotNumber + 2n, interfaceId, paddingType.END); @@ -1285,19 +1335,35 @@ async function getProtocolStatusPrivateContractState(protocolDiamondAddress) { preUpgradeVersions = getVersionsBeforeTarget(Object.keys(facets.upgrade), versionTags.newVersion); } + // initializedVersions const initializedVersionsState = []; for (const version of preUpgradeVersions) { const storageSlot = getMappingStoragePosition(protocolStatusStorageSlotNumber + 3n, version, paddingType.END); initializedVersionsState.push(await getStorageAt(protocolDiamondAddress, storageSlot)); } - return { pauseScenario, reentrancyStatus, initializedInterfacesState, initializedVersionsState }; + // incomingVoucherId + const incomingVoucherId = await getStorageAt(protocolDiamondAddress, protocolStatusStorageSlotNumber + 5n); + expect(incomingVoucherId).to.equal(ZeroHash); + + // incomingVoucherCloneAddress + const incomingVoucherCloneAddress = await getStorageAt(protocolDiamondAddress, protocolStatusStorageSlotNumber + 6n); + expect(incomingVoucherId).to.equal(ZeroHash); + + return { + pauseScenario, + reentrancyStatus, + initializedInterfacesState, + initializedVersionsState, + incomingVoucherId, + incomingVoucherCloneAddress, + }; } async function getProtocolLookupsPrivateContractState( protocolDiamondAddress, { mockToken, mockTwinTokens }, - { sellers, DRs, agents, buyers, offers, groups, twins }, + { sellers, DRs, agents, buyers, offers, groups, twins, royaltyRecipients }, groupHandler ) { /* @@ -1341,6 +1407,9 @@ async function getProtocolLookupsPrivateContractState( #34 [X] // placeholder for additionalCollections #35 [ ] // placeholder for sellerSalt #36 [ ] // placeholder for isUsedSellerSalt + #37 [X] // placeholder for royaltyRecipientsBySeller + #38 [ ] // placeholder for royaltyRecipientIndexBySellerAndRecipient + #39 [ ] // placeholder for royaltyRecipientIdByWallet */ // starting slot @@ -1660,6 +1729,40 @@ async function getProtocolLookupsPrivateContractState( ); } + let royaltyRecipientIndexBySellerAndRecipient = []; + let royaltyRecipientIdByWallet = []; + // royaltyRecipientIndexBySellerAndRecipient, royaltyRecipientIdByWallet + for (const royaltyRecipient of royaltyRecipients) { + const { wallet: royaltyRecipientWallet } = royaltyRecipient; + const royaltyRecipientAddress = royaltyRecipientWallet.address; + + // royaltyRecipientIndexBySellerAndRecipient + const royaltyRecipientIndexRecipient = []; + for (const seller of sellers) { + const { id } = seller; + const firstMappingStorageSlot = await getStorageAt( + protocolDiamondAddress, + getMappingStoragePosition(protocolLookupsSlotNumber + 38n, id, paddingType.START) + ); + + royaltyRecipientIndexRecipient.push( + await getStorageAt( + protocolDiamondAddress, + getMappingStoragePosition(firstMappingStorageSlot, royaltyRecipientAddress, paddingType.START) + ) + ); + royaltyRecipientIndexBySellerAndRecipient.push(royaltyRecipientIndexRecipient); + } + + // royaltyRecipientIdByWallet + royaltyRecipientIdByWallet.push( + getStorageAt( + protocolDiamondAddress, + getMappingStoragePosition(protocolLookupsSlotNumber + 39n, royaltyRecipientAddress, paddingType.START) + ) + ); + } + return { exchangeIdsByOfferState, groupIdByOfferState, @@ -1684,6 +1787,57 @@ async function getProtocolLookupsPrivateContractState( }; } +async function getProtocolEntitiesPrivateContractState(protocolDiamondAddress, { exchanges, royaltyRecipients }) { + /* + ProtocolEntities storage layout + + #0-#18 [X] // placeholders for entites {offers}....{authTokens} + #19 [ ] // placeholder for exchangeCosts + #20 [ ] // placeholder for royaltyRecipients + */ + + // starting slot + const protocolEntitiesStorageSlot = keccak256(toUtf8Bytes("boson.protocol.entities")); + const protocolEntitiesStorageSlotNumber = BigInt(protocolEntitiesStorageSlot); + + // exchangeCosts + const exchangeCosts = []; + for (const exchange of exchanges) { + let exchangeCostByExchange = []; + const id = exchange.exchangeId; + const arraySlot = getMappingStoragePosition(protocolEntitiesStorageSlotNumber + 19n, id, paddingType.START); + const arrayLength = await getStorageAt(protocolDiamondAddress, arraySlot); + const arrayStart = BigInt(keccak256(arraySlot)); + const structLength = 5n; // BosonTypes.ExchangeCost has 2 fields + for (let i = 0n; i < arrayLength; i++) { + const ExchangeCost = []; + for (let j = 0n; j < structLength; j++) { + ExchangeCost.push(await getStorageAt(protocolDiamondAddress, BigInt(arrayStart) + i * structLength + j)); + } + exchangeCostByExchange.push(ExchangeCost); + } + exchangeCosts.push(exchangeCostByExchange); + } + + // royaltyRecipients + const royaltyRecipientStructs = []; + const structLength = 2n; // BosonTypes.RoyaltyRecipient has 2 fields + for (const royaltyRecipient of royaltyRecipients) { + const { id } = royaltyRecipient; + const storageSlot = BigInt( + getMappingStoragePosition(protocolEntitiesStorageSlotNumber + 20n, id, paddingType.START) + ); + const royaltyRecipientStruct = []; + for (let i = 0n; i < structLength; i++) { + royaltyRecipientStruct.push(await getStorageAt(protocolDiamondAddress, storageSlot + i)); + } + + royaltyRecipientStructs.push(royaltyRecipientStruct); + } + + return { exchangeCosts, royaltyRecipientStructs }; +} + async function getStorageLayout(contractName) { const { sourceName } = await hre.artifacts.readArtifact(contractName); const buildInfo = await hre.artifacts.getBuildInfo(`${sourceName}:${contractName}`); @@ -1693,12 +1847,13 @@ async function getStorageLayout(contractName) { return storage; } -function compareStorageLayouts(storageBefore, storageAfter, equalCustomTypes) { +function compareStorageLayouts(storageBefore, storageAfter, equalCustomTypes, renamedVariables) { // 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; for (const stateVariableBefore of storageBefore) { - const { label } = stateVariableBefore; + let { label } = stateVariableBefore; + label = renamedVariables[label] || label; if (label == "__gap") { // __gap is special variable that does not store any data and can potentially be modified // TODO: if changed, validate against new variables @@ -1886,7 +2041,11 @@ async function populateVoucherContract( for (let i = 0; i < sellers.length; i++) { for (let j = i; j >= 0; j--) { // Mock offer, offerDates and offerDurations - const { offer, offerDates, offerDurations } = await mockOffer(); + const offerStructV2_3 = versionsBelowV2_4.includes(isBefore ? versionTags.oldVersion : versionTags.newVersion); + const { offer, offerDates, offerDurations } = await mockOffer({ + refreshModule: true, + legacyOffer: offerStructV2_3, + }); // Set unique offer properties based on offer id offer.id = `${offerId}`; @@ -1909,19 +2068,33 @@ async function populateVoucherContract( // Set unique offerDurations based on offer id offerDurations.disputePeriod = `${(offerId + 1) * Number(oneMonth)}`; offerDurations.voucherValid = `${(offerId + 1) * Number(oneMonth)}`; - offerDurations.resolutionPeriod = `${(offerId + 1) * Number(oneDay)}`; + offerDurations.resolutionPeriod = `${(offerId + 1) * Number(oneDay) + Number(oneWeek)}`; // choose one DR and agent const disputeResolverId = DRs[0].disputeResolver.id; const agentId = "0"; const offerFeeLimit = MaxUint256; // unlimited offer fee to not affect the tests + const royaltyInfo = [ + { + bps: [`${sellers[j].voucherInitValues.royaltyPercentage}`], + recipients: [ZeroAddress], + }, + ]; + // create an offer - await offerHandler - .connect(sellers[j].wallet) - .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId, offerFeeLimit); + if (offerStructV2_3) { + await offerHandler + .connect(sellers[j].wallet) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId); + } else { + offer.royaltyInfo = royaltyInfo; + await offerHandler + .connect(sellers[j].wallet) + .createOffer(offer, offerDates, offerDurations, disputeResolverId, agentId, offerFeeLimit); + } - offers.push({ offer, offerDates, offerDurations, disputeResolverId, agentId }); + offers.push({ offer, offerDates, offerDurations, disputeResolverId, agentId, royaltyInfo }); sellers[j].offerIds.push(offerId); // Deposit seller funds so the commit will succeed @@ -1974,7 +2147,7 @@ async function populateVoucherContract( .commitToOffer(await buyerWallet.getAddress(), offer.id, { value: msgValue }); } - exchanges.push({ exchangeId: exchangeId, offerId: offer.id, buyerIndex: j }); + exchanges.push({ exchangeId: exchangeId, offerId: offer.id, buyerIndex: j, sellerId: offer.sellerId }); exchangeId++; } } @@ -1995,17 +2168,20 @@ async function getVoucherContractState({ bosonVouchers, exchanges, sellers, buye ); // no arg getters - const [sellerId, contractURI, getRoyaltyPercentage, owner, name, symbol] = await Promise.all([ + const [sellerId, contractURI, owner, name, symbol] = await Promise.all([ bosonVoucher.getSellerId(), bosonVoucher.contractURI(), - bosonVoucher.getRoyaltyPercentage(), bosonVoucher.owner(), bosonVoucher.name(), bosonVoucher.symbol(), ]); // tokenId related - const tokenIds = exchanges.map((exchange) => exchange.exchangeId); // tokenId and exchangeId are interchangeable + const bosonVoucherAddress = await bosonVoucher.getAddress(); + const { id } = sellers.find((s) => s.voucherContractAddress.toLowerCase() == bosonVoucherAddress.toLowerCase()); + const tokenIds = exchanges + .filter((exchange) => exchange.sellerId == id) + .map((exchange) => deriveTokenId(exchange.offerId, exchange.exchangeId)); const ownerOf = await Promise.all( tokenIds.map((tokenId) => bosonVoucher.ownerOf(tokenId).catch(() => "invalid token")) ); @@ -2033,7 +2209,6 @@ async function getVoucherContractState({ bosonVouchers, exchanges, sellers, buye supportstInterface, sellerId, contractURI, - getRoyaltyPercentage, owner, name, symbol,