Skip to content

Commit

Permalink
2.4.0 migration script and upgrade tests (#923)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
zajck and albertfolch-redeemeum committed Mar 8, 2024
1 parent be6ff08 commit 5166a27
Show file tree
Hide file tree
Showing 13 changed files with 2,110 additions and 61 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions hardhat.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
2 changes: 1 addition & 1 deletion scripts/config/role-assignments.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
297 changes: 297 additions & 0 deletions scripts/migrations/migrate_2.4.0.js
Original file line number Diff line number Diff line change
@@ -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;
14 changes: 12 additions & 2 deletions scripts/upgrade-facets.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Loading

0 comments on commit 5166a27

Please sign in to comment.