UNPKG

@vechain/vebetterdao-contracts

Version:

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

780 lines 141 kB
import { ethers } from "hardhat"; import { expect } from "chai"; import { catchRevert, createProposalAndExecuteIt, filterEventsByName, getOrDeployContractInstances, getVot3Tokens, moveToCycle, parseAllocationVoteCastEvent, parseRoundStartedEvent, startNewAllocationRound, waitForRoundToEnd, bootstrapEmissions, getProposalIdFromTx, waitForProposalToBeActive, waitForVotingPeriodToEnd, bootstrapAndStartEmissions, waitForCurrentRoundToEnd, ZERO_ADDRESS, waitForNextBlock, payDeposit, waitForBlock, } from "./helpers"; import { describe, it, before } from "mocha"; import { getImplementationAddress } from "@openzeppelin/upgrades-core"; import { deployAndUpgrade, deployProxy, upgradeProxy } from "../scripts/helpers"; import { endorseApp } from "./helpers/xnodes"; import { createLocalConfig } from "@repo/config/contracts/envs/local"; import { createTestConfig } from "./helpers/config"; import { xAllocationVotingLibraries } from "../scripts/libraries/xAllocationVotingLibraries"; describe("X-Allocation Voting - @shard14-core", function () { // Environment params let creator1; let creator2; let creator3; let creator4; before(async function () { const { creators } = await getOrDeployContractInstances({ forceDeploy: true }); creator1 = creators[0]; creator2 = creators[1]; creator3 = creators[2]; creator4 = creators[3]; }); describe("Deployment", function () { it("Admins should be set correctly", async function () { const { xAllocationVoting, owner, timeLock } = await getOrDeployContractInstances({ forceDeploy: true, }); const ADMIN_ROLE = "0x0000000000000000000000000000000000000000000000000000000000000000"; expect(await xAllocationVoting.hasRole(ADMIN_ROLE, await timeLock.getAddress())).to.eql(true); expect(await xAllocationVoting.hasRole(ADMIN_ROLE, owner.address)).to.eql(true); }); it("Should not support invalid interface", async function () { const { xAllocationVoting } = await getOrDeployContractInstances({ forceDeploy: true, }); const INVALID_ID = "0xffffffff"; expect(await xAllocationVoting.supportsInterface(INVALID_ID)).to.eql(false); }); it("Can set multiple admins during deployment", async function () { const { voterRewards, timeLock, emissions, x2EarnApps, vot3, otherAccounts, owner } = await getOrDeployContractInstances({ forceDeploy: false, }); const xAllocationVotingV1 = (await deployProxy("XAllocationVotingV1", [ { vot3Token: await vot3.getAddress(), quorumPercentage: 1, initialVotingPeriod: 2, timeLock: await timeLock.getAddress(), voterRewards: await voterRewards.getAddress(), emissions: await emissions.getAddress(), admins: [await timeLock.getAddress(), otherAccounts[2].address, otherAccounts[2].address], upgrader: owner.address, contractsAddressManager: otherAccounts[2].address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: 2, appSharesCap: 2, votingThreshold: BigInt(1), }, ])); // Upgrade XAllocationVoting V1 to XAllocationVoting V2 const xAllocationVoting = (await upgradeProxy("XAllocationVotingV1", "XAllocationVotingV2", await xAllocationVotingV1.getAddress(), [], { version: 2 })); expect(await xAllocationVoting.hasRole(await xAllocationVoting.DEFAULT_ADMIN_ROLE(), await timeLock.getAddress())).to.eql(true); expect(await xAllocationVoting.hasRole(await xAllocationVoting.DEFAULT_ADMIN_ROLE(), otherAccounts[2].address)).to.eql(true); expect(await xAllocationVoting.hasRole(await xAllocationVoting.DEFAULT_ADMIN_ROLE(), otherAccounts[2].address)).to.eql(true); }); it("Should support ERC 165 interface", async () => { const { xAllocationVoting } = await getOrDeployContractInstances({ forceDeploy: true }); expect(await xAllocationVoting.supportsInterface("0x01ffc9a7")).to.equal(true); // ERC165 }); it("Clock mode is set correctly", async function () { const { xAllocationVoting, vot3 } = await getOrDeployContractInstances({ forceDeploy: false, }); expect(await xAllocationVoting.CLOCK_MODE()).to.eql(await vot3.CLOCK_MODE()); }); it("Clock returns block number if token does not implement clock function", async function () { const { xAllocationVoting, timeLock, voterRewards, emissions, otherAccounts, x2EarnApps, b3tr, owner } = await getOrDeployContractInstances({ forceDeploy: false, }); let clock = await xAllocationVoting.clock(); expect(parseInt(clock.toString())).to.eql(await ethers.provider.getBlockNumber()); const xAllocationVotingWithB3TRV1 = (await deployProxy("XAllocationVotingV1", [ { vot3Token: await b3tr.getAddress(), quorumPercentage: 1, initialVotingPeriod: 2, timeLock: await timeLock.getAddress(), voterRewards: await voterRewards.getAddress(), emissions: await emissions.getAddress(), admins: [await timeLock.getAddress(), otherAccounts[2].address, otherAccounts[2].address], upgrader: owner.address, contractsAddressManager: otherAccounts[2].address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: 2, appSharesCap: 2, votingThreshold: BigInt(1), }, ])); // Upgrade XAllocationVoting V1 to XAllocationVoting V2 const xAllocationVotingWithB3TR = (await upgradeProxy("XAllocationVotingV1", "XAllocationVotingV2", await xAllocationVotingWithB3TRV1.getAddress(), [], { version: 2 })); clock = await xAllocationVotingWithB3TR.clock(); expect(parseInt(clock.toString())).to.eql(await ethers.provider.getBlockNumber()); //CLOKC_MODE should return "mode=blocknumber&from=default" expect(await xAllocationVotingWithB3TR.CLOCK_MODE()).to.eql("mode=blocknumber&from=default"); }); it("Should revert if VOT3 is set to zero address in initilisation", async () => { const config = createLocalConfig(); const { owner, x2EarnApps, timeLock, emissions, voterRewards } = await getOrDeployContractInstances({ forceDeploy: true, config, }); await expect(deployProxy("XAllocationVotingV1", [ { vot3Token: ZERO_ADDRESS, quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, // X Alloc voting period timeLock: await timeLock.getAddress(), voterRewards: await voterRewards.getAddress(), emissions: await emissions.getAddress(), admins: [await timeLock.getAddress(), owner.address], upgrader: owner.address, contractsAddressManager: owner.address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, }, ])).to.be.reverted; }); it("Should revert if VoterRewards is set to zero address in initilisation", async () => { const config = createLocalConfig(); const { owner, x2EarnApps, timeLock, emissions, vot3 } = await getOrDeployContractInstances({ forceDeploy: true, config, }); await expect(deployProxy("XAllocationVotingV1", [ { vot3Token: await vot3.getAddress(), quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, // X Alloc voting period timeLock: await timeLock.getAddress(), voterRewards: ZERO_ADDRESS, emissions: await emissions.getAddress(), admins: [await timeLock.getAddress(), owner.address], upgrader: owner.address, contractsAddressManager: owner.address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, }, ])).to.be.reverted; }); it("Should revert if Emissions is set to zero address in initilisation", async () => { const config = createLocalConfig(); const { owner, x2EarnApps, timeLock, vot3, voterRewards } = await getOrDeployContractInstances({ forceDeploy: true, config, }); await expect(deployProxy("XAllocationVotingV1", [ { vot3Token: await vot3.getAddress(), quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, // X Alloc voting period timeLock: await timeLock.getAddress(), voterRewards: await voterRewards.getAddress(), emissions: ZERO_ADDRESS, admins: [await timeLock.getAddress(), owner.address], upgrader: owner.address, contractsAddressManager: owner.address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, }, ])).to.be.reverted; }); it("Should revert if an admin is set to zero address in initilisation", async () => { const config = createLocalConfig(); const { owner, x2EarnApps, timeLock, vot3, voterRewards, emissions } = await getOrDeployContractInstances({ forceDeploy: true, config, }); await expect(deployProxy("XAllocationVotingV1", [ { vot3Token: await vot3.getAddress(), quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, // quorum percentage initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, // X Alloc voting period timeLock: await timeLock.getAddress(), voterRewards: await voterRewards.getAddress(), emissions: await emissions.getAddress(), admins: [await timeLock.getAddress(), ZERO_ADDRESS], upgrader: owner.address, contractsAddressManager: owner.address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, }, ])).to.be.reverted; }); }); describe("Contract upgradeablity", () => { it("Admin should be able to upgrade the contract", async function () { const { xAllocationVoting, owner } = await getOrDeployContractInstances({ forceDeploy: true, }); const xAllocLibs = await xAllocationVotingLibraries(); // Deploy the implementation contract const Contract = await ethers.getContractFactory("XAllocationVoting", { libraries: { AutoVotingLogic: await xAllocLibs.AutoVotingLogic.getAddress(), ExternalContractsUtils: await xAllocLibs.ExternalContractsUtils.getAddress(), VotingSettingsUtils: await xAllocLibs.VotingSettingsUtils.getAddress(), VotesUtils: await xAllocLibs.VotesUtils.getAddress(), VotesQuorumFractionUtils: await xAllocLibs.VotesQuorumFractionUtils.getAddress(), RoundEarningsSettingsUtils: await xAllocLibs.RoundEarningsSettingsUtils.getAddress(), RoundFinalizationUtils: await xAllocLibs.RoundFinalizationUtils.getAddress(), RoundsStorageUtils: await xAllocLibs.RoundsStorageUtils.getAddress(), RoundVotesCountingUtils: await xAllocLibs.RoundVotesCountingUtils.getAddress(), }, }); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); const currentImplAddress = await getImplementationAddress(ethers.provider, await xAllocationVoting.getAddress()); const UPGRADER_ROLE = await xAllocationVoting.UPGRADER_ROLE(); expect(await xAllocationVoting.hasRole(UPGRADER_ROLE, owner.address)).to.eql(true); await expect(xAllocationVoting.connect(owner).upgradeToAndCall(await implementation.getAddress(), "0x")).to.not.be .reverted; const newImplAddress = await getImplementationAddress(ethers.provider, await xAllocationVoting.getAddress()); expect(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase()); expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()); }); it("Only admin should be able to upgrade the contract", async function () { const { xAllocationVoting, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true, }); // Deploy the implementation contract const Contract = await ethers.getContractFactory("TimeLock"); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); const currentImplAddress = await getImplementationAddress(ethers.provider, await xAllocationVoting.getAddress()); const UPGRADER_ROLE = await xAllocationVoting.UPGRADER_ROLE(); expect(await xAllocationVoting.hasRole(UPGRADER_ROLE, otherAccount.address)).to.eql(false); await expect(xAllocationVoting.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")).to .be.reverted; const newImplAddress = await getImplementationAddress(ethers.provider, await xAllocationVoting.getAddress()); expect(newImplAddress.toUpperCase()).to.eql(currentImplAddress.toUpperCase()); expect(newImplAddress.toUpperCase()).to.not.eql((await implementation.getAddress()).toUpperCase()); }); it("Admin can change UPGRADER_ROLE", async function () { const { xAllocationVoting, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true, }); // Deploy the implementation contract const Contract = await ethers.getContractFactory("TimeLock"); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); const currentImplAddress = await getImplementationAddress(ethers.provider, await xAllocationVoting.getAddress()); const UPGRADER_ROLE = await xAllocationVoting.UPGRADER_ROLE(); expect(await xAllocationVoting.hasRole(UPGRADER_ROLE, otherAccount.address)).to.eql(false); await expect(xAllocationVoting.connect(owner).grantRole(UPGRADER_ROLE, otherAccount.address)).to.not.be.reverted; await expect(xAllocationVoting.connect(owner).revokeRole(UPGRADER_ROLE, owner.address)).to.not.be.reverted; await expect(xAllocationVoting.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")).to .not.be.reverted; const newImplAddress = await getImplementationAddress(ethers.provider, await xAllocationVoting.getAddress()); expect(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase()); expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()); }); it("should be able to upgrade the xAllocationVoting contract through governance", async function () { const config = createLocalConfig(); config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD = 0; const { xAllocationVoting, timeLock, governor, owner, otherAccount, vot3, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, config, }); const xAllocLibs = await xAllocationVotingLibraries(); await getVot3Tokens(otherAccount, "1000"); await vot3.connect(otherAccount).approve(await governor.getAddress(), "1000"); const UPGRADER_ROLE = await xAllocationVoting.UPGRADER_ROLE(); await expect(xAllocationVoting.connect(owner).grantRole(UPGRADER_ROLE, await timeLock.getAddress())).to.not.be .reverted; // Deploy the implementation contract const Contract = await ethers.getContractFactory("XAllocationVoting", { libraries: { AutoVotingLogic: await xAllocLibs.AutoVotingLogic.getAddress(), ExternalContractsUtils: await xAllocLibs.ExternalContractsUtils.getAddress(), VotingSettingsUtils: await xAllocLibs.VotingSettingsUtils.getAddress(), VotesUtils: await xAllocLibs.VotesUtils.getAddress(), VotesQuorumFractionUtils: await xAllocLibs.VotesQuorumFractionUtils.getAddress(), RoundEarningsSettingsUtils: await xAllocLibs.RoundEarningsSettingsUtils.getAddress(), RoundFinalizationUtils: await xAllocLibs.RoundFinalizationUtils.getAddress(), RoundsStorageUtils: await xAllocLibs.RoundsStorageUtils.getAddress(), RoundVotesCountingUtils: await xAllocLibs.RoundVotesCountingUtils.getAddress(), }, }); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); await bootstrapAndStartEmissions(); // Whitelist user await veBetterPassport.whitelist(otherAccount.address); await veBetterPassport.toggleCheck(1); const LatestContract = await ethers.getContractAt("XAllocationVoting", await xAllocationVoting.getAddress()); // Now we can create a proposal const encodedFunctionCall = LatestContract.interface.encodeFunctionData("upgradeToAndCall", [ await implementation.getAddress(), "0x", ]); const description = "Upgrading XAllocationVoting contracts"; const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description)); const currentRoundId = await xAllocationVoting.currentRoundId(); const tx = await governor .connect(owner) .propose([await xAllocationVoting.getAddress()], [0], [encodedFunctionCall], description, currentRoundId + 1n, 0); const proposalId = await getProposalIdFromTx(tx); await waitForProposalToBeActive(proposalId); await governor.connect(otherAccount).castVote(proposalId, 1); await waitForVotingPeriodToEnd(proposalId); expect(await governor.state(proposalId)).to.eql(4n); // succeded await governor.queue([await xAllocationVoting.getAddress()], [0], [encodedFunctionCall], descriptionHash); expect(await governor.state(proposalId)).to.eql(5n); await governor.execute([await xAllocationVoting.getAddress()], [0], [encodedFunctionCall], descriptionHash); expect(await governor.state(proposalId)).to.eql(6n); const newImplAddress = await getImplementationAddress(ethers.provider, await xAllocationVoting.getAddress()); expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()); }); it("Cannot initialize twice", async function () { const { voterRewards, timeLock, emissions, x2EarnApps, vot3, otherAccounts, owner } = await getOrDeployContractInstances({ forceDeploy: false, }); const xAllocationVotingV1 = (await deployProxy("XAllocationVotingV1", [ { vot3Token: await vot3.getAddress(), quorumPercentage: 1, initialVotingPeriod: 2, timeLock: await timeLock.getAddress(), voterRewards: await voterRewards.getAddress(), emissions: await emissions.getAddress(), admins: [await timeLock.getAddress(), otherAccounts[2].address, otherAccounts[2].address], upgrader: owner.address, contractsAddressManager: otherAccounts[2].address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: 2, appSharesCap: 2, votingThreshold: BigInt(1), }, ])); await catchRevert(xAllocationVotingV1.initialize({ vot3Token: await vot3.getAddress(), quorumPercentage: 1, initialVotingPeriod: 2, timeLock: await timeLock.getAddress(), voterRewards: await voterRewards.getAddress(), emissions: await emissions.getAddress(), admins: [await timeLock.getAddress(), otherAccounts[2].address, otherAccounts[2].address], upgrader: owner.address, contractsAddressManager: otherAccounts[2].address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: 2, appSharesCap: 2, votingThreshold: BigInt(1), })); }); it("Should return correct version of the contract", async () => { const { xAllocationVoting } = await getOrDeployContractInstances({ forceDeploy: true, }); expect(await xAllocationVoting.version()).to.equal("10"); }); it("Should not break storage when upgrading to V2, V3, V4, V5, V6, V7 and V8", async () => { const config = createTestConfig(); const configContracts = await getOrDeployContractInstances({ forceDeploy: true, }); const { otherAccounts, x2EarnApps, xAllocationPool, b3tr, vot3, galaxyMember, timeLock, treasury, owner, veBetterPassport, minterAccount, } = configContracts; // set personhood threshold to 0 await veBetterPassport.connect(owner).setThresholdPoPScore(0); await veBetterPassport.toggleCheck(4); const emissionsV1 = (await deployProxy("Emissions", [ { minter: minterAccount.address, admin: owner.address, upgrader: owner.address, contractsAddressManager: owner.address, decaySettingsManager: owner.address, b3trAddress: await b3tr.getAddress(), destinations: [ await xAllocationPool.getAddress(), owner.address, await treasury.getAddress(), config.MIGRATION_ADDRESS, ], initialXAppAllocation: config.INITIAL_X_ALLOCATION, cycleDuration: config.EMISSIONS_CYCLE_DURATION, decaySettings: [ config.EMISSIONS_X_ALLOCATION_DECAY_PERCENTAGE, config.EMISSIONS_VOTE_2_EARN_DECAY_PERCENTAGE, config.EMISSIONS_X_ALLOCATION_DECAY_PERIOD, config.EMISSIONS_VOTE_2_EARN_ALLOCATION_DECAY_PERIOD, ], treasuryPercentage: config.EMISSIONS_TREASURY_PERCENTAGE, maxVote2EarnDecay: config.EMISSIONS_MAX_VOTE_2_EARN_DECAY_PERCENTAGE, migrationAmount: config.MIGRATION_AMOUNT, }, ])); const emissions = (await upgradeProxy("EmissionsV1", "Emissions", await emissionsV1.getAddress(), [config.EMISSIONS_IS_NOT_ALIGNED ?? false], { version: 2, })); const voterRewardsV1 = (await deployProxy("VoterRewardsV1", [ owner.address, // admin owner.address, // upgrader owner.address, // contractsAddressManager await emissions.getAddress(), await galaxyMember.getAddress(), await b3tr.getAddress(), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [0, 10, 20, 50, 100, 150, 200, 400, 900, 2400], ])); const voterRewards = (await upgradeProxy("VoterRewardsV1", "VoterRewards", await voterRewardsV1.getAddress(), [], { version: 2, })); // Set vote 2 earn (VoterRewards deployed contract) address in emissions await emissions.connect(owner).setVote2EarnAddress(await voterRewards.getAddress()); // Deploy and upgrade through V3 const xAllocationVotingV3 = (await deployAndUpgrade(["XAllocationVotingV1", "XAllocationVotingV2", "XAllocationVotingV3"], [ [ { vot3Token: await vot3.getAddress(), quorumPercentage: config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE, initialVotingPeriod: config.EMISSIONS_CYCLE_DURATION - 1, timeLock: await timeLock.getAddress(), voterRewards: await voterRewards.getAddress(), emissions: await emissions.getAddress(), admins: [await timeLock.getAddress(), owner.address], upgrader: owner.address, contractsAddressManager: owner.address, x2EarnAppsAddress: await x2EarnApps.getAddress(), baseAllocationPercentage: config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE, appSharesCap: config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP, votingThreshold: config.X_ALLOCATION_VOTING_VOTING_THRESHOLD, }, ], [await veBetterPassport.getAddress()], [], ], { versions: [undefined, 2, 3], })); expect(await xAllocationVotingV3.version()).to.equal("3"); await emissions.setXAllocationsGovernorAddress(await xAllocationVotingV3.getAddress()); expect(await emissions.xAllocationsGovernor()).to.eql(await xAllocationVotingV3.getAddress()); await xAllocationPool.setXAllocationVotingAddress(await xAllocationVotingV3.getAddress()); expect(await xAllocationPool.xAllocationVoting()).to.eql(await xAllocationVotingV3.getAddress()); await xAllocationPool.setEmissionsAddress(await emissions.getAddress()); expect(await xAllocationPool.emissions()).to.eql(await emissions.getAddress()); // Grant Vote registrar role to XAllocationVoting await voterRewards .connect(owner) .grantRole(await voterRewards.VOTE_REGISTRAR_ROLE(), await xAllocationVotingV3.getAddress()); // Grant admin role to voter rewards for registering x allocation voting await xAllocationVotingV3 .connect(owner) .grantRole(await xAllocationVotingV3.DEFAULT_ADMIN_ROLE(), emissions.getAddress()); //Set the emissions address and the admin as the ROUND_STARTER_ROLE in XAllocationVoting const roundStarterRole = await xAllocationVotingV3.ROUND_STARTER_ROLE(); await xAllocationVotingV3 .connect(owner) .grantRole(roundStarterRole, await emissions.getAddress()) .then(async (tx) => await tx.wait()); await xAllocationVotingV3 .connect(owner) .grantRole(roundStarterRole, owner.address) .then(async (tx) => await tx.wait()); const user1 = otherAccounts[0]; const user2 = otherAccounts[1]; const user3 = otherAccounts[2]; // fund wallets await getVot3Tokens(user1, "1000"); await getVot3Tokens(user2, "1000"); await getVot3Tokens(user3, "1000"); // add apps const app1Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[2].address)); const app2Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[3].address)); const app3Id = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[4].address)); await x2EarnApps .connect(creator1) .submitApp(otherAccounts[2].address, otherAccounts[2].address, otherAccounts[2].address, "metadataURI"); await x2EarnApps .connect(creator2) .submitApp(otherAccounts[3].address, otherAccounts[3].address, otherAccounts[3].address, "metadataURI"); await x2EarnApps .connect(creator3) .submitApp(otherAccounts[4].address, otherAccounts[4].address, otherAccounts[4].address, "metadataURI"); await endorseApp(app1Id, otherAccounts[2]); await endorseApp(app2Id, otherAccounts[3]); await endorseApp(app3Id, otherAccounts[4]); // 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(); // start round await emissions.connect(minterAccount).start(); expect(await xAllocationVotingV3.currentRoundId()).to.equal(1n); // make people vote await xAllocationVotingV3.connect(user1).castVote(1, [app1Id], [ethers.parseEther("100")]); await xAllocationVotingV3 .connect(user2) .castVote(1, [app1Id, app2Id], [ethers.parseEther("100"), ethers.parseEther("200")]); // Upgrade to V4 (using the V3 contract address) const xAllocationVotingV4 = (await upgradeProxy("XAllocationVotingV3", "XAllocationVotingV4", await xAllocationVotingV3.getAddress(), [], { version: 4, })); const xAllocationVotingV5 = (await upgradeProxy("XAllocationVotingV4", "XAllocationVotingV5", await xAllocationVotingV4.getAddress(), // Use V4's address [], { version: 5, })); expect(await xAllocationVotingV5.version()).to.equal("5"); // check that round is ok expect(await xAllocationVotingV5.currentRoundId()).to.equal(1n); expect(await xAllocationVotingV5.state(1n)).to.equal(0n); // Active expect(await xAllocationVotingV5.hasVoted(1, user1.address)).to.be.true; expect(await xAllocationVotingV5.hasVoted(1, user2.address)).to.be.true; expect(await xAllocationVotingV5.hasVoted(1, user3.address)).to.be.false; expect(await xAllocationVotingV5.getAppVotes(1, app1Id)).to.equal(ethers.parseEther("200")); expect(await xAllocationVotingV5.getAppVotes(1, app2Id)).to.equal(ethers.parseEther("200")); expect(await xAllocationVotingV5.getAppVotes(1, app3Id)).to.equal(ethers.parseEther("0")); // check that can still vote on the new round await xAllocationVotingV5.connect(user3).castVote(1, [app1Id], [ethers.parseEther("100")]); expect(await xAllocationVotingV5.getAppVotes(1, app1Id)).to.equal(ethers.parseEther("300")); // check that round is over correctly const blockNextCycle = await emissions.getNextCycleBlock(); await waitForBlock(Number(blockNextCycle)); expect(await emissions.isCycleEnded(1)).to.be.true; await emissions.distribute(); expect(await xAllocationVotingV4.currentRoundId()).to.equal(2n); // check that rewards are distributed correctly await expect(xAllocationPool.claim(1, app1Id)).to.not.be.reverted; await expect(xAllocationPool.claim(1, app2Id)).to.not.be.reverted; await expect(xAllocationPool.claim(1, app3Id)).to.not.be.reverted; // can cast vote for round 2 await xAllocationVotingV4.connect(user1).castVote(2, [app1Id], [ethers.parseEther("100")]); // Upgrade to V5 (using the V4 contract address) const xAllocationVotingV6 = (await upgradeProxy("XAllocationVotingV5", "XAllocationVotingV6", await xAllocationVotingV4.getAddress(), // Use V4's address [], { version: 6, })); expect(await xAllocationVotingV6.version()).to.equal("6"); // check that round is ok expect(await xAllocationVotingV6.currentRoundId()).to.equal(2n); expect(await xAllocationVotingV6.state(2n)).to.equal(0n); // Active expect(await xAllocationVotingV6.hasVoted(1, user1.address)).to.be.true; expect(await xAllocationVotingV6.hasVoted(1, user2.address)).to.be.true; expect(await xAllocationVotingV6.hasVoted(1, user3.address)).to.be.true; // Update these expectations to match the actual state after user3's vote expect(await xAllocationVotingV6.getAppVotes(1, app1Id)).to.equal(ethers.parseEther("300")); expect(await xAllocationVotingV6.getAppVotes(1, app2Id)).to.equal(ethers.parseEther("200")); expect(await xAllocationVotingV6.getAppVotes(1, app3Id)).to.equal(ethers.parseEther("0")); // check that can still vote on the new round await xAllocationVotingV6.connect(user3).castVote(2, [app1Id], [ethers.parseEther("100")]); expect(await xAllocationVotingV6.getAppVotes(2, app1Id)).to.equal(ethers.parseEther("200")); // check that round is over correctly await waitForBlock(Number(await emissions.getNextCycleBlock())); expect(await emissions.isCycleEnded(2)).to.be.true; await emissions.distribute(); expect(await xAllocationVotingV6.currentRoundId()).to.equal(3n); // check that rewards are distributed correctly await expect(xAllocationPool.claim(2, app1Id)).to.not.be.reverted; await expect(xAllocationPool.claim(2, app2Id)).to.not.be.reverted; await expect(xAllocationPool.claim(2, app3Id)).to.not.be.reverted; // can cast vote for round 3 await xAllocationVotingV6.connect(user1).castVote(3, [app1Id], [ethers.parseEther("100")]); // Upgrade to V7 (using the V6 contract address) const xAllocationVotingV7 = (await upgradeProxy("XAllocationVotingV6", "XAllocationVotingV7", await xAllocationVotingV6.getAddress(), // Use V6's address [], { version: 7, })); expect(await xAllocationVotingV7.version()).to.equal("7"); // check that round is ok expect(await xAllocationVotingV7.currentRoundId()).to.equal(3n); expect(await xAllocationVotingV7.state(3n)).to.equal(0n); // Active const xAllocLibs = await xAllocationVotingLibraries(); // Latest version const xAllocationVoting = (await upgradeProxy("XAllocationVotingV7", "XAllocationVoting", await xAllocationVotingV7.getAddress(), // Use V7's address [], { version: 8, libraries: { AutoVotingLogic: await xAllocLibs.AutoVotingLogic.getAddress(), ExternalContractsUtils: await xAllocLibs.ExternalContractsUtils.getAddress(), VotingSettingsUtils: await xAllocLibs.VotingSettingsUtils.getAddress(), VotesUtils: await xAllocLibs.VotesUtils.getAddress(), VotesQuorumFractionUtils: await xAllocLibs.VotesQuorumFractionUtils.getAddress(), RoundEarningsSettingsUtils: await xAllocLibs.RoundEarningsSettingsUtils.getAddress(), RoundFinalizationUtils: await xAllocLibs.RoundFinalizationUtils.getAddress(), RoundsStorageUtils: await xAllocLibs.RoundsStorageUtils.getAddress(), RoundVotesCountingUtils: await xAllocLibs.RoundVotesCountingUtils.getAddress(), }, })); expect(await xAllocationVoting.version()).to.equal("10"); }); }); describe("Settings", function () { describe("General settings", function () { it("Contract should not be able to receive ether", async function () { const { xAllocationVoting, owner } = await getOrDeployContractInstances({ forceDeploy: false }); await expect(owner.sendTransaction({ to: await xAllocationVoting.getAddress(), value: ethers.parseEther("1.0"), // Sends exactly 1.0 ether })).to.be.reverted; expect(await ethers.provider.getBalance(await xAllocationVoting.getAddress())).to.eql(0n); }); describe("emissions address", function () { it("Admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set a new emissions contract correctly", async function () { const { xAllocationVoting, owner, emissions } = await getOrDeployContractInstances({ forceDeploy: true, }); await bootstrapAndStartEmissions(); expect(await xAllocationVoting.hasRole(await xAllocationVoting.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address)).to.be.true; const oldEmissionsAddress = await emissions.getAddress(); const emissionsSetIface = new ethers.Interface([ "event EmissionsSet(address oldContractAddress, address newContractAddress)", ]); const receipt = await (await xAllocationVoting.connect(owner).setEmissionsAddress(owner.address)).wait(); let emissionsSetEvent; for (const log of receipt?.logs ?? []) { try { const p = emissionsSetIface.parseLog({ topics: [...log.topics], data: log.data }); if (p !== null && p.name === "EmissionsSet") { emissionsSetEvent = p; break; } } catch { /* ignore non-matching logs */ } } expect(emissionsSetEvent).to.not.be.undefined; expect(emissionsSetEvent.args.oldContractAddress).to.equal(oldEmissionsAddress); expect(emissionsSetEvent.args.newContractAddress).to.equal(owner.address); }); it("Cannot set a new emissions contract to zero address", async function () { const { xAllocationVoting, owner } = await getOrDeployContractInstances({ forceDeploy: true, }); await bootstrapAndStartEmissions(); await expect(xAllocationVoting.connect(owner).setEmissionsAddress(ZERO_ADDRESS)).to.be.reverted; }); it("Only admin with CONTRACTS_ADDRESS_MANAGER_ROLE should be able to set a new emissions contract", async function () { const { xAllocationVoting, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true, }); expect(await xAllocationVoting.hasRole(await xAllocationVoting.CONTRACTS_ADDRESS_MANAGER_ROLE(), otherAccount.address)).to.be.false; await expect(xAllocationVoting.connect(otherAccount).setEmissionsAddress(otherAccount.address)).to.be.reverted; }); }); describe("x2EarnApps address", function () { it("Admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set x2EarnApps address correctly", async function () { const { xAllocationVoting, owner, x2EarnApps } = await getOrDeployContractInstances({ forceDeploy: true, }); expect(await xAllocationVoting.hasRole(await xAllocationVoting.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address)).to.be.true; const oldX2EarnAppsAddress = await x2EarnApps.getAddress(); const tx = await xAllocationVoting.connect(owner).setX2EarnAppsAddress(owner.address); const x2EarnAppsSetIface = new ethers.Interface([ "event X2EarnAppsSet(address oldContractAddress, address newContractAddress)", ]); const receipt = await tx.wait(); let x2EarnAppsSetEvent; for (const log of receipt?.logs ?? []) { try { const p = x2EarnAppsSetIface.parseLog({ topics: [...log.topics], data: log.data }); if (p !== null && p.name === "X2EarnAppsSet") { x2EarnAppsSetEvent = p; break; } } catch { /* ignore non-matching logs */ } } expect(x2EarnAppsSetEvent).to.not.be.undefined; expect(x2EarnAppsSetEvent.args.oldContractAddress).to.equal(oldX2EarnAppsAddress); expect(x2EarnAppsSetEvent.args.newContractAddress).to.equal(owner.address); }); it("Cannot set x2EarnApps address to zero address", async function () { const { xAllocationVoting, owner } = await getOrDeployContractInstances({ forceDeploy: true, }); await expect(xAllocationVoting.connect(owner).setX2EarnAppsAddress(ZERO_ADDRESS)).to.be.reverted; }); it("Only admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set x2EarnApps address", async function () { const { xAllocationVoting, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true, }); expect(await xAllocationVoting.hasRole(await xAllocationVoting.CONTRACTS_ADDRESS_MANAGER_ROLE(), otherAccount.address)).to.be.false; await expect(xAllocationVoting.connect(otherAccount).setX2EarnAppsAddress(otherAccount.address)).to.be .reverted; }); }); describe("VoterRewards address", function () { it("Admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set voter rewards address correctly", async function () { const { xAllocationVoting, owner, voterRewards } = await getOrDeployContractInstances({ forceDeploy: true, }); expect(await xAllocationVoting.hasRole(await xAllocationVoting.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address)).to.be.true; const oldVoterRewardsAddress = await voterRewards.getAddress(); const tx = await xAllocationVoting.connect(owner).setVoterRewardsAddress(owner.address); const voterRewardsSetIface = new ethers.Interface([ "event VoterRewardsSet(address oldContractAddress, address newContractAddress)", ]); const receipt = await tx.wait(); let voterRewardsSetEvent; for (const log of receipt?.logs ?? []) { try { const p = voterRewardsSetIface.parseLog({ topics: [...log.topics], data: log.data }); if (p !== null && p.name === "VoterRewardsSet") { voterRewardsSetEvent = p; break; } } catch { /* ignore non-matching logs */ } } expect(voterRewardsSetEvent).to.not.be.undefined; expect(voterRewardsSetEvent.args.oldContractAddress).to.equal(oldVoterRewardsAddress); expect(voterRewardsSetEvent.args.newContractAddress).to.equal(owner.address); }); it("Cannot set voter rewards address to zero address", async function () { const { xAllocationVoting, owner } = await getOrDeployContractInstances({ forceDeploy: true, }); await expect(xAllocationVoting.connect(owner).setVoterRewardsAddress(ZERO_ADDRESS)).to.be.reverted; }); it("Only admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set voter rewards address", async function () { const { xAllocationVoting, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true, }); expect(await xAllocationVoting.hasRole(await xAllocationVoting.CONTRACTS_ADDRESS_MANAGER_ROLE(), otherAccount.address)).to.be.false; await expect(xAllocationVoting.connect(otherAccount).setVoterRewardsAddress(otherAccount.address)).to.be .reverted; }); }); it("Can get and set veBetterPassport address", async function () { const { xAllocationVoting, owner, otherAccount, veBetterPassport } = await getOrDeployContractInstances({ forceDeploy: true, }); const oldVeBetterPassportAddress = await veBetterPassport.getAddress(); // assign governance role to owner await xAllocationVoting.grantRole(await xAllocationVoting.GOVERNANCE_ROLE(), owner.address); expect(await xAllocationVoting.hasRole(await xAllocationVoting.GOVERNANCE_ROLE(), owner.address)).to.be.true; const tx = await xAllocationVoting.connect(owner).setVeBetterPassport(owner.address); const veBetterPassportSetIface = new ethers.Interface([ "event VeBetterPassportSet(address oldContractAddress, address newContractAddress)", ]); const receipt = await tx.wait(); let veBetterPassportSetEvent; for (const log of receipt?.logs ?? []) { try { const p = veBetterPassportSetIface.parseLog({ topics: [...log.topics], data: log.data }); if (p !== null && p.name === "VeBetterPassportSet") { veBetterPassportSetEvent = p; break; } } catch { /* ignore non-matching logs */ } } expect(veBetterPassportSetEvent).to.not.be.undefined; expect(veBetterPassportSetEvent.args.oldContractAddress).to.equal(oldVeBetterPassportAddress); expect(veBetterPassportSetEvent.args.newContractAddress).to.equal(owner.address); // only GOVERNANCE_ROLE can set the veBetterPassport address expect(await xAllocationVoting.hasRole(await xAllocationVoting.GOVERNANCE_ROLE(), otherAccount.address)).to.be .false; await expect(xAllocationVoting.connect(otherAccount).setVeBetterPassport(otherAccount.address)).to.be.reverted; }); }); describe("Voting threshold", function () { it("can update voting threshold through governance", async function () { const