UNPKG

@vechain/vebetterdao-contracts

Version:

Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.

728 lines (727 loc) 34.8 kB
import { ethers, network } from "hardhat"; import { Errors__factory, } from "../../typechain-types"; import { getOrDeployContractInstances } from "./deploy"; import { mine } from "@nomicfoundation/hardhat-network-helpers"; import { Clause, Units, VTHO } from "@vechain/sdk-core"; import { TransactionUtils } from "@repo/utils"; import { getConfig } from "@repo/config"; import { ThorClient } from "@vechain/sdk-network"; import { getTestKeys } from "../../scripts/helpers/seedAccounts"; import { endorseApp } from "./xnodes"; import { time } from "@nomicfoundation/hardhat-network-helpers"; import { createLocalConfig } from "@repo/config/contracts/envs/local"; const thorClient = ThorClient.at(getConfig().nodeUrl); export const waitForNextBlock = async () => { if (network.name === "hardhat") { await mine(1); return; } const accounts = getTestKeys(2); const source = accounts[0]; const target = accounts[1]; if (!source.pk) throw new Error("No private key"); // since we do not support ethers' evm_mine yet, do a vet transaction to force a block const clauses = []; clauses.push(Clause.transferVTHOToken(target.address, VTHO.of(1, Units.wei))); await TransactionUtils.sendTx(thorClient, clauses, source.pk); }; export const moveBlocks = async (blocks) => { for (let i = 0; i < blocks; i++) { await waitForNextBlock(); } }; // Allow overriding fixture contracts instead of redeploying each time export const getContractInstances = async (overrides) => { const instances = await getOrDeployContractInstances({}); if (!instances) throw new Error("Failed to get contract instances"); return { ...instances, ...overrides, }; }; export const getRoundId = async (contractToPassToMethods) => { const { emissions, xAllocationVoting } = await getContractInstances(contractToPassToMethods); if ((await emissions.nextCycle()) === 0n) { await bootstrapAndStartEmissions(contractToPassToMethods); } const roundId = ((await xAllocationVoting.currentRoundId()) + 1n).toString(); return roundId; }; export const createProposal = async (contractToCall, ContractFactory, proposer, description = "", functionTocall = "tokenDetails", values = [], roundId) => { const deployInstances = await getOrDeployContractInstances({}); const xAllocationVoting = deployInstances?.xAllocationVoting; const governor = deployInstances?.governor; const emissions = deployInstances?.emissions; if (!xAllocationVoting || !governor || !emissions) throw new Error("Deploy instances are not correctly set"); if (!roundId) { // to ensure that test will work correctly before creating a proposal we wait for current round to end // and start a new one if ((await emissions.nextCycle()) === 0n) { // if emissions are not started yet, we need to bootstrap and start them await bootstrapAndStartEmissions(); } else { // otherwise we need to wait for the current round to end and start the next one await waitForCurrentRoundToEnd(); await emissions.distribute(); } roundId = ((await xAllocationVoting.currentRoundId()) + 1n).toString(); } const address = await contractToCall.getAddress(); const encodedFunctionCall = ContractFactory.interface.encodeFunctionData(functionTocall, values); const tx = await governor .connect(proposer) .propose([address], [0], [encodedFunctionCall], description, roundId.toString(), 0, { gasLimit: 10000000, }); return tx; }; export const createGrantProposal = async (proposer, targets, calldatas, values = [], description, depositAmount, grantsReceiver, milestonesDetailsMetadataURI, contractToPassToMethods, roundId) => { const { governor } = await getContractInstances(contractToPassToMethods); if (!roundId) { roundId = await getRoundId(contractToPassToMethods); } const noDepositAmountFromGrantee = 0; const tx = await governor .connect(proposer) .proposeGrant(targets, values, calldatas, description, roundId, noDepositAmountFromGrantee, grantsReceiver, milestonesDetailsMetadataURI, { gasLimit: 10000000, }); return tx; }; export const createMultiContractProposalGrant = async (proposer, calldatas, values, targets, description, depositAmount, grantsReceiver, milestonesDetailsMetadataURI, roundId, contractToPassToMethods) => { const { governor } = await getContractInstances(contractToPassToMethods); if (!roundId) { roundId = await getRoundId(contractToPassToMethods); } const tx = await governor .connect(proposer) .proposeGrant(targets, values, calldatas, description, roundId.toString(), depositAmount, grantsReceiver, milestonesDetailsMetadataURI, { gasLimit: 10000000, }); return tx; }; export const createProposalWithMultipleFunctionsAndExecuteItGrant = async (proposer, voter, contractsToCall, Contract, description, functionsToCall, args, depositAmount, grantsReceiver, milestonesDetailsMetadataURI, contractToPassToMethods, roundId) => { const { governor, veBetterPassport, owner, treasury } = await getContractInstances(contractToPassToMethods); await veBetterPassport.whitelist(voter.address); if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1); // load votes // console.log("Loading votes") await getVot3Tokens(voter, "300000", contractToPassToMethods); await waitForNextBlock(); if (!roundId) { roundId = await getRoundId(contractToPassToMethods); } const targets = []; const calldatas = []; const values = []; for (let i = 0; i < functionsToCall.length; i++) { const func = functionsToCall[i]; const argsForFunc = args[i]; targets.push(await treasury.getAddress()); calldatas.push(Contract.interface.encodeFunctionData(func, argsForFunc)); values.push(0n); } const noDepositAmountFromGrantee = 0; depositAmount = noDepositAmountFromGrantee; // create a new proposal // console.log("Creating proposal") const tx = await createMultiContractProposalGrant(proposer, calldatas, values, targets, description, depositAmount, grantsReceiver, milestonesDetailsMetadataURI, roundId, contractToPassToMethods); // change the all function to be compatible with grants proposal const proposalId = await getProposalIdFromGrantsProposalTx(tx, contractToPassToMethods); await payDeposit(proposalId, owner, contractToPassToMethods); // wait // console.log("Waiting for voting period to start") await waitForProposalToBeActive(proposalId, contractToPassToMethods); // vote // console.log("Voting") await governor.connect(voter).castVote(proposalId, 1, { gasLimit: 10000000 }); // vote for // wait // console.log("Waiting for voting period to end") await waitForVotingPeriodToEnd(proposalId, contractToPassToMethods); // queue it // console.log("Queueing") const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)); await governor.queue(targets, values, calldatas, descriptionHash, { gasLimit: 10000000, }); await waitForNextBlock(); // for all the contracts, whitelist the function for (const contract of contractsToCall) { await governor .connect(owner) .setWhitelistFunction(await contract.getAddress(), contract.interface.getFunction("transferB3TR")?.selector, true); } // execute it // console.log("Executing") await governor.connect(owner).execute(targets, values, calldatas, descriptionHash, { gasLimit: 10000000, }); return { proposalId, tx }; }; export const createProposalWithMultipleFunctions = async (proposer, contractToCalls, Contract, description, functionsToCall, args, roundId) => { const { governor, emissions, xAllocationVoting } = await getOrDeployContractInstances({}); if (!roundId) { // to ensure that test will work correctly before creating a proposal we wait for current round to end // and start a new one if ((await emissions.nextCycle()) === 0n) { // if emissions are not started yet, we need to bootstrap and start them await bootstrapAndStartEmissions(); } else { // otherwise we need to wait for the current round to end and start the next one await waitForCurrentRoundToEnd(); await emissions.distribute(); } roundId = ((await xAllocationVoting.currentRoundId()) + 1n).toString(); } // create a new proposal const tx = await governor.connect(proposer).propose(contractToCalls, Array(functionsToCall.length).fill(0), functionsToCall.map((func, index) => { return Contract.interface.encodeFunctionData(func, args[index]); }), description, roundId, 0); return tx; }; export const getProposalIdFromTx = async (tx, depositPayed = false, contractToPassToMethods) => { const { governor } = await getContractInstances(contractToPassToMethods); const proposeReceipt = await tx.wait(); const event = depositPayed ? proposeReceipt?.logs[3] : proposeReceipt?.logs[0]; const decodedLogs = governor.interface.parseLog({ topics: [...event?.topics], data: event ? event.data : "", }); return decodedLogs?.args[0]; }; export const getProposalIdFromGrantsProposalTx = async (tx, contractToPassToMethods) => { const { governor } = await getContractInstances(contractToPassToMethods); const proposeReceipt = await tx.wait(); // Find the ProposalCreated event const proposalCreatedEvent = proposeReceipt?.logs.find(log => { try { const decoded = governor.interface.parseLog({ topics: [...log.topics], data: log.data, }); return decoded?.name === "ProposalCreated"; } catch (e) { return false; } }); if (!proposalCreatedEvent) { throw new Error("ProposalCreated event not found"); } // Parse the ProposalCreated event const decodedEvent = governor.interface.parseLog({ topics: [...proposalCreatedEvent.topics], data: proposalCreatedEvent.data, }); // The proposal ID is the first argument const proposalId = decodedEvent?.args[0]; return proposalId; }; export const payDeposit = async (proposalId, depositer, contractToPassToMethods) => { const { governor, vot3 } = await getContractInstances(contractToPassToMethods); // get the proposal deposit amount const proposalThreshold = await governor.proposalDepositThreshold(proposalId); const vot3Balance = await vot3.balanceOf(depositer.address); if (proposalThreshold > vot3Balance) { //The proposer needs to have some delegated VOT3 to be able to create a proposal await getVot3Tokens(depositer, ethers.formatEther(proposalThreshold)); // We also need to wait a block to update the proposer's votes snapshot await waitForNextBlock(); } await vot3.connect(depositer).approve(await governor.getAddress(), proposalThreshold); await governor.connect(depositer).deposit(proposalThreshold, proposalId); }; export const waitForVotingPeriodToEnd = async (proposalId, contractToPassToMethods) => { const { governor } = await getContractInstances(contractToPassToMethods); const deadline = await governor.proposalDeadline(proposalId); const currentBlock = await governor.clock(); await moveBlocks(parseInt((deadline - currentBlock + BigInt(1)).toString())); }; export const waitForRoundToEnd = async (roundId, xAllocationVoting) => { const instance = await getOrDeployContractInstances({ forceDeploy: false, }); if (!xAllocationVoting) xAllocationVoting = instance.xAllocationVoting; if (typeof roundId === "bigint") roundId = parseInt(roundId.toString()); if (typeof roundId !== "number") throw new Error("Invalid roundId"); const deadline = await xAllocationVoting.roundDeadline(roundId); const currentBlock = await xAllocationVoting.clock(); await moveBlocks(parseInt((deadline - currentBlock + BigInt(1)).toString())); }; export const waitForCurrentRoundToEnd = async (contractToPassToMethods) => { const { xAllocationVoting } = await getContractInstances(contractToPassToMethods); const currentRoundId = await xAllocationVoting.currentRoundId(); await waitForRoundToEnd(Number(currentRoundId), xAllocationVoting); await waitForNextBlock(); }; export const waitForProposalToBeActive = async (proposalId, contractToPassToMethods) => { const { governor } = await getContractInstances(contractToPassToMethods); let proposalState = await governor.state(proposalId); // proposal id of the proposal in the beforeAll step if (proposalState.toString() !== "1") { await moveToCycle(parseInt((await governor.proposalStartRound(proposalId)).toString()) + 1, contractToPassToMethods); // Update the proposal state proposalState = await governor.state(proposalId); } return proposalState; }; /** * Calls the timelock to see if the operation is ready * * @param proposalId the proposal id */ export const waitForQueuedProposalToBeReady = async (proposalId) => { const { timeLock, governor } = await getOrDeployContractInstances({}); const timelockId = await governor.getTimelockId(proposalId); let isOperationReady = await timeLock.isOperationReady(timelockId); do { await moveBlocks(1); isOperationReady = await timeLock.isOperationReady(timelockId); } while (isOperationReady === false); }; // Mint some B3TR and Convert B3TR for VOT3 export const getVot3Tokens = async (receiver, amount, contractToPassToMethods) => { const { b3tr, vot3, minterAccount } = await getContractInstances(contractToPassToMethods); // Mint some B3TR await b3tr.connect(minterAccount).mint(receiver, ethers.parseEther(amount)); // Approve VOT3 to spend B3TR on behalf of otherAccount. N.B. this is an important step and could be included in a multi clause transaction await b3tr.connect(receiver).approve(await vot3.getAddress(), ethers.parseEther(amount)); // Lock B3TR to get VOT3 await vot3.connect(receiver).convertToVOT3(ethers.parseEther(amount)); }; export const updateGMMultipliers = async () => { const config = createLocalConfig(); const { voterRewards, owner } = await getOrDeployContractInstances({}); for (let i = 0; i < config.VOTER_REWARDS_LEVELS_V2.length; i++) { const level = config.VOTER_REWARDS_LEVELS_V2[i]; const multiplier = config.GM_MULTIPLIERS_V2[i]; // Update the multiplier for the level await voterRewards.connect(owner).setLevelToMultiplier(level, multiplier); } }; export const createProposalAndExecuteIt = async (proposer, voter, contractToCall, Contract, description, functionToCall, args = [], roundId) => { const { governor, veBetterPassport } = await getOrDeployContractInstances({}); // console.log("Loading votes"); await getVot3Tokens(voter, "30000"); await waitForNextBlock(); await veBetterPassport.whitelist(voter.address); if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1); // create a new proposal // console.log("Creating proposal"); const tx = await createProposal(contractToCall, Contract, proposer, description, functionToCall, args, roundId); const proposalId = await getProposalIdFromTx(tx); await payDeposit(proposalId, proposer); // wait // console.log("Waiting for voting period to start"); await waitForProposalToBeActive(proposalId); // vote // console.log("Voting"); await governor.connect(voter).castVote(proposalId, 1, { gasLimit: 10000000 }); // vote for // wait // console.log("Waiting for voting period to end"); await waitForVotingPeriodToEnd(proposalId); // queue it // console.log("Queueing"); const encodedFunctionCall = Contract.interface.encodeFunctionData(functionToCall, args); const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)); await governor.queue([await contractToCall.getAddress()], [0], [encodedFunctionCall], descriptionHash, { gasLimit: 10000000, }); await waitForNextBlock(); // execute it // console.log("Executing"); const extecutionTX = await governor.execute([await contractToCall.getAddress()], [0], [encodedFunctionCall], descriptionHash, { gasLimit: 10000000, }); return extecutionTX; }; export const createProposalWithMultipleFunctionsAndExecuteIt = async (proposer, voter, contractsToCall, Contract, description, functionsToCall, args, roundId) => { const { governor, emissions, xAllocationVoting, veBetterPassport } = await getOrDeployContractInstances({}); await veBetterPassport.whitelist(voter.address); if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1); // load votes // console.log("Loading votes"); await getVot3Tokens(voter, "30000"); await waitForNextBlock(); if (!roundId) { // to ensure that test will work correctly before creating a proposal we wait for current round to end // and start a new one if ((await emissions.nextCycle()) === 0n) { // if emissions are not started yet, we need to bootstrap and start them await bootstrapAndStartEmissions(); } else { // otherwise we need to wait for the current round to end and start the next one await waitForCurrentRoundToEnd(); await emissions.distribute(); } roundId = ((await xAllocationVoting.currentRoundId()) + 1n).toString(); } // Encode functions const encodedFunctionCalls = functionsToCall.map((func, index) => { return Contract.interface.encodeFunctionData(func, args[index]); }); // create a new proposal const tx = await createProposalWithMultipleFunctions(proposer, contractsToCall, Contract, description, functionsToCall, args, roundId); // change the all function to be compatible with grants proposal const proposalId = await getProposalIdFromGrantsProposalTx(tx); await payDeposit(proposalId, proposer); // wait // console.log("Waiting for voting period to start"); await waitForProposalToBeActive(proposalId); // vote // console.log("Voting"); await governor.connect(voter).castVote(proposalId, 1, { gasLimit: 10000000 }); // vote for // wait // console.log("Waiting for voting period to end"); await waitForVotingPeriodToEnd(proposalId); // queue it // console.log("Queueing"); const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)); await governor.queue(contractsToCall, Array(functionsToCall.length).fill(0), encodedFunctionCalls, descriptionHash, { gasLimit: 10000000, }); await waitForNextBlock(); // execute it console.log("Executing"); await governor.execute(contractsToCall, Array(functionsToCall.length).fill(0), encodedFunctionCalls, descriptionHash, { gasLimit: 10000000, }); }; export const waitForBlock = async (blockNumber) => { const currentBlock = await ethers.provider.getBlock(await ethers.provider.getBlockNumber()); if (!currentBlock?.number) throw new Error("Could not get current block number"); if (currentBlock?.number < blockNumber) { // Get blocks required to wait const blocksToWait = blockNumber - currentBlock?.number; if (blocksToWait > 0) await moveBlocks(blocksToWait); } }; export const waitForNextCycle = async (emission) => { if (!emission) { const { emissions } = await getContractInstances(); emission = emissions; } const blockNextCycle = await emission.getNextCycleBlock(); await waitForBlock(Number(blockNextCycle)); }; /** * It will move to the desired cycle without actually distribute it. * E.g: we are in cycle 1 (distributed) and want to move to cycle 3 (not distributed) then we call this funciton with cycle 3 * and it will distribute the cycle 2 and stop before distributing the cycle 3 */ export const moveToCycle = async (cycle, contractToPassToMethods) => { const { emissions, minterAccount } = await getContractInstances(contractToPassToMethods); const cycleToBeDistributed = await emissions.nextCycle(); for (let i = 0; i < BigInt(cycle) - cycleToBeDistributed; i++) { await waitForNextCycle(emissions); await emissions.connect(minterAccount).distribute(); } }; export const voteOnApps = async (apps, voters, votes, roundId, xAllocationVoting, veBetterPassport) => { const instance = await getOrDeployContractInstances({}); if (!veBetterPassport) veBetterPassport = instance.veBetterPassport; if (!xAllocationVoting) xAllocationVoting = instance.xAllocationVoting; if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1); for (let i = 0; i < voters.length; i++) { const voter = voters[i]; const voterVotes = votes[i]; await veBetterPassport.whitelist(voter.address); // Filter out both zero votes and their corresponding apps const filteredData = apps .map((app, index) => ({ app, vote: voterVotes[index], })) .filter(data => data.vote !== BigInt(0)); // If there are any valid votes left, proceed with voting if (filteredData.length > 0) { const validApps = filteredData.map(data => data.app); const validVotes = filteredData.map(data => data.vote); // Execute the vote with the filtered non-zero votes and corresponding apps await xAllocationVoting.connect(voter).castVote(roundId, validApps, validVotes); } } }; export const addAppsToAllocationVoting = async (apps, owner) => { const { x2EarnApps, otherAccounts } = await getOrDeployContractInstances({}); let appIds = []; let i = 0; for (const app of apps) { await x2EarnApps.connect(owner).submitApp(app, app, app, "metadataURI"); const appId = await x2EarnApps.hashAppName(app); appIds.push(appId); await endorseApp(appId, otherAccounts[i]); i++; } return appIds; }; export const startNewAllocationRound = async (contractToPassToMethods) => { const { emissions, xAllocationVoting, minterAccount } = await getContractInstances(contractToPassToMethods); const nextCycle = await emissions.nextCycle(); if (nextCycle === 0n) { await bootstrapAndStartEmissions(); } else if (nextCycle === 1n) { await emissions.connect(minterAccount).start(); } else if (await emissions.isCycleEnded(await emissions.getCurrentCycle())) { await emissions.distribute(); } return Number(await xAllocationVoting.currentRoundId()); }; export const calculateBaseAllocationOffChain = async (roundId) => { const { emissions, xAllocationVoting } = await getOrDeployContractInstances({}); // Amount available for this round (assuming the amount is already scaled by 1e18 for precision) let totalAmount = await emissions.getXAllocationAmount(roundId); let elegibleApps = await xAllocationVoting.getAppIdsOfRound(roundId); const baseAllcoationPercentage = await xAllocationVoting.getRoundBaseAllocationPercentage(roundId); let remaining = (totalAmount * baseAllcoationPercentage) / BigInt(100); let amountPerApp = remaining / BigInt(elegibleApps.length); return amountPerApp; }; export const calculateVariableAppAllocationOffChain = async (roundId, appId) => { const { emissions, xAllocationVoting, xAllocationPool } = await getOrDeployContractInstances({}); // Amount available for this round (assuming the amount is already scaled by 1e18 for precision) let totalAmount = await emissions.getXAllocationAmount(roundId); let totalAvailable = (totalAmount * (BigInt(100) - (await xAllocationVoting.getRoundBaseAllocationPercentage(roundId)))) / BigInt(100); const roundAppShares = await xAllocationPool.getAppShares(roundId, appId); let appShares = roundAppShares[0] / BigInt(100); return (totalAvailable * appShares) / BigInt(100); }; export const calculateUnallocatedAppAllocationOffChain = async (roundId, appId) => { const { emissions, xAllocationVoting, xAllocationPool } = await getOrDeployContractInstances({}); // Amount available for this round (assuming the amount is already scaled by 1e18 for precision) let totalAmount = await emissions.getXAllocationAmount(roundId); let totalAvailable = (totalAmount * (BigInt(100) - (await xAllocationVoting.getRoundBaseAllocationPercentage(roundId)))) / BigInt(100); const roundAppShares = await xAllocationPool.getAppShares(roundId, appId); let appShares = roundAppShares[1] / BigInt(100); return (totalAvailable * appShares) / BigInt(100); }; export const participateInAllocationVoting = async (user, waitRoundToEnd = false, endorser) => { const { xAllocationVoting, x2EarnApps, owner, veBetterPassport, x2EarnCreator } = await getOrDeployContractInstances({}); await getVot3Tokens(user, "1"); await getVot3Tokens(owner, "1000"); await veBetterPassport.whitelist(user.address); if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1); // Get or create app ID let appId; const appsAlreadySubmitted = await x2EarnApps.isCreatorOfAnyApp(user.address); if (!appsAlreadySubmitted) { // Create new app const appName = "App" + Math.random(); if ((await x2EarnCreator.balanceOf(user.address)) === 0n) { await x2EarnCreator.connect(owner).safeMint(user.address); } await x2EarnApps.connect(user).submitApp(user.address, user.address, appName, "metadataURI"); appId = await x2EarnApps.hashAppName(appName); await endorseApp(appId, endorser || owner); } else { // We will work with the already submitted app const allEligibleApps = await x2EarnApps.allEligibleApps(); for (const app of allEligibleApps) { const creators = await x2EarnApps.appCreators(app); if (creators.length > 0 && creators[0].toLowerCase() === user.address.toLowerCase()) { appId = app; break; } } // If no app found for this user, use first eligible app as fallback if (!appId && allEligibleApps.length > 0) { appId = allEligibleApps[0]; console.log("Using fallback app:", appId); } else if (!appId) { console.warn("No eligible apps found for user"); return; } } // Start round and vote (common for both paths) const roundId = await startNewAllocationRound(); await xAllocationVoting.connect(user).castVote(roundId, [appId], [ethers.parseEther("1")]); if (waitRoundToEnd) { await waitForRoundToEnd(roundId); } }; export const participateInGovernanceVoting = async (user, admin, contractToCall, Contract, description, functionToCall, args = [], waitProposalToEnd = false) => { const { governor, veBetterPassport } = await getOrDeployContractInstances({}); await getVot3Tokens(user, "1"); await getVot3Tokens(admin, "1000"); await veBetterPassport.connect(admin).whitelist(user.address); if ((await veBetterPassport.isCheckEnabled(1)) === false) await veBetterPassport.toggleCheck(1); const tx = await createProposal(contractToCall, Contract, admin, description, functionToCall, args); const proposalId = await getProposalIdFromTx(tx); // pay for the deposit await payDeposit(proposalId, admin); await waitForProposalToBeActive(proposalId); // Vote await governor.connect(user).castVote(proposalId, 1); if (waitProposalToEnd) { await waitForVotingPeriodToEnd(proposalId); } }; export const bootstrapEmissions = async (contractToPassToMethods) => { const { b3tr, owner, emissions, minterAccount } = await getContractInstances(contractToPassToMethods); // Grant minter role to emissions contract await b3tr.connect(owner).grantRole(await b3tr.MINTER_ROLE(), await emissions.getAddress()); // Bootstrap emissions await emissions.connect(minterAccount).bootstrap(); }; export const bootstrapAndStartEmissions = async (contractToPassToMethods) => { const { emissions, minterAccount } = await getContractInstances(contractToPassToMethods); await bootstrapEmissions(contractToPassToMethods); // Start emissions await emissions.connect(minterAccount).start(); }; export const upgradeNFTtoLevel = async (tokenId, level, nft, b3tr, owner, minter) => { const currentLevel = await nft.levelOf(tokenId); for (let i = currentLevel; i < level; i++) { await upgradeNFTtoNextLevel(tokenId, nft, b3tr, owner, minter); } }; export const upgradeNFTtoNextLevel = async (tokenId, nft, b3tr, owner, minter) => { const b3trToUpgrade = await nft.getB3TRtoUpgrade(tokenId); await b3tr.connect(minter).mint(owner.address, b3trToUpgrade); await b3tr.connect(owner).approve(await nft.getAddress(), b3trToUpgrade); await nft.connect(owner).upgrade(tokenId); }; /** * Helper function to get storage slots. * @param contractAddress The address of the contract. * @param initialSlots The initial storage slots. * @returns Array of storage slots. */ export const getStorageSlots = async (contractAddress, ...initialSlots) => { const slots = []; for (const initialSlot of initialSlots) { for (let i = initialSlot; i < initialSlot + BigInt(100); i++) { slots.push(await ethers.provider.getStorage(contractAddress, i)); } } return slots.filter(slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000"); // Removing empty slots }; export const mintLegacyNode = async (level, owner) => { const { vechainNodesMock } = await getOrDeployContractInstances({}); if (!vechainNodesMock) throw new Error("VechainNodesMock not found"); const blockNumBefore = await ethers.provider.getBlockNumber(); const blockBefore = await ethers.provider.getBlock(blockNumBefore); if (!blockBefore) throw new Error("Block before not found"); const timestampBefore = blockBefore.timestamp; const nextBlockTimestamp = timestampBefore + 1000; await time.setNextBlockTimestamp(nextBlockTimestamp); await vechainNodesMock.addToken(owner.address, level, false, 0, 0); return [ owner.address, BigInt(level), false, false, ethers.toBigInt(nextBlockTimestamp), ethers.toBigInt(nextBlockTimestamp), ethers.toBigInt(nextBlockTimestamp), ]; }; export const delegateWithSignature = async (veBetterPassport, delegator, delegatee, deadlineFromNow) => { const blockNumber = await ethers.provider.getBlockNumber(); const currentBlockTimestamp = (await ethers.provider.getBlock(blockNumber))?.timestamp; if (!currentBlockTimestamp) throw new Error("Could not get current block timestamp"); // Calculate the deadline const deadline = currentBlockTimestamp + deadlineFromNow; // Set up EIP-712 domain const domain = { name: "VeBetterPassport", version: "1", chainId: 1337, verifyingContract: await veBetterPassport.getAddress(), }; let types = { Delegation: [ { name: "delegator", type: "address" }, { name: "delegatee", type: "address" }, { name: "deadline", type: "uint256" }, ], }; // Prepare the struct to sign const delegationData = { delegator: delegator.address, delegatee: delegatee.address, deadline, }; // Create the EIP-712 signature for the delegator const signature = await delegator.signTypedData(domain, types, delegationData); // Perform the delegation using the signature await veBetterPassport.connect(delegatee).delegateWithSignature(delegator.address, deadline, signature); }; export const linkEntityToPassportWithSignature = async (veBetterPassport, passport, entity, deadlineFromNow) => { const blockNumber = await ethers.provider.getBlockNumber(); const currentBlockTimestamp = (await ethers.provider.getBlock(blockNumber))?.timestamp; if (!currentBlockTimestamp) throw new Error("Could not get current block timestamp"); // Calculate the deadline const deadline = currentBlockTimestamp + deadlineFromNow; // Set up EIP-712 domain const domain = { name: "VeBetterPassport", version: "1", chainId: 1337, verifyingContract: await veBetterPassport.getAddress(), }; let types = { LinkEntity: [ { name: "entity", type: "address" }, { name: "passport", type: "address" }, { name: "deadline", type: "uint256" }, ], }; // Prepare the struct to sign const delegationData = { entity: entity.address, passport: passport.address, deadline, }; // Create the EIP-712 signature for the delegator const signature = await entity.signTypedData(domain, types, delegationData); // Perform the delegation using the signature await veBetterPassport.connect(passport).linkEntityToPassportWithSignature(entity.address, deadline, signature); }; export const getTwoUniqueRandomIndices = (max) => { const firstIndex = Math.floor(Math.random() * max); let secondIndex; do { secondIndex = Math.floor(Math.random() * max); } while (secondIndex === firstIndex); return [firstIndex, secondIndex]; }; // Since we moved errors to a separate library, our tests expect the // error from the library instead of from the contract. // The most common pattern people use in their tests when using library // errors is to create a test helper like this one. export const getStargateNFTErrorsInterface = async (_stargateNFTContract) => { const { stargateNftMock } = await getOrDeployContractInstances({ forceDeploy: false, }); const addressToUse = _stargateNFTContract ? await _stargateNFTContract.getAddress() : await stargateNftMock.getAddress(); return Errors__factory.connect(addressToUse, ethers.provider); };