UNPKG

@vechain/vebetterdao-contracts

Version:

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

750 lines (749 loc) 175 kB
import { describe, it, before } from "mocha"; import { catchRevert, getOrDeployContractInstances, getVot3Tokens, levels, multipliers, waitForNextCycle, voteOnApps, addAppsToAllocationVoting, waitForRoundToEnd, bootstrapEmissions, upgradeNFTtoLevel, waitForNextBlock, createProposal, getProposalIdFromTx, waitForProposalToBeActive, ZERO_ADDRESS, participateInAllocationVoting, startNewAllocationRound, mintLegacyNode, bootstrapAndStartEmissions, payDeposit, updateGMMultipliers, } from "./helpers"; import { expect } from "chai"; import { ethers } from "hardhat"; import { createLocalConfig } from "@repo/config/contracts/envs/local"; import { createTestConfig } from "./helpers/config"; import { getImplementationAddress } from "@openzeppelin/upgrades-core"; import { deployAndUpgrade, deployProxy, upgradeProxy } from "../scripts/helpers"; import { time } from "@nomicfoundation/hardhat-network-helpers"; import { createNodeHolder, endorseApp } from "./helpers/xnodes"; describe("VoterRewards - @shard10-core", () => { // Environment params let creator1; let creator2; before(async function () { const { creators } = await getOrDeployContractInstances({ forceDeploy: true }); creator1 = creators[0]; creator2 = creators[1]; }); describe("Contract parameters", () => { it("Should have correct parameters set on deployment", async () => { const { voterRewards, owner, galaxyMember, emissions } = await getOrDeployContractInstances({ forceDeploy: true }); // Contract address checks expect(await voterRewards.emissions()).to.equal(await emissions.getAddress()); expect(await voterRewards.galaxyMember()).to.equal(await galaxyMember.getAddress()); // Admin role expect(await voterRewards.hasRole(await voterRewards.DEFAULT_ADMIN_ROLE(), owner.address)).to.equal(true); // NFT Levels multipliers for (const level of levels) { expect(await voterRewards.levelToMultiplier(level)).to.equal(multipliers[levels.indexOf(level)]); } }); it("Should be able to set new emissions contract", async () => { const { voterRewards, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await voterRewards.connect(owner).setEmissions(otherAccount.address); expect(await voterRewards.emissions()).to.equal(otherAccount.address); }); it("Should not be able to set new emissions contract if not admin", async () => { const { voterRewards, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await expect(voterRewards.connect(otherAccount).setEmissions(otherAccount.address)).to.be.reverted; }); it("Should be able to set new Galaxy Member contract", async () => { const { voterRewards, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await voterRewards.connect(owner).setGalaxyMember(otherAccount.address); expect(await voterRewards.galaxyMember()).to.equal(otherAccount.address); }); it("Should not be able to set new Galaxy Member contract if not admin", async () => { const { voterRewards, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await expect(voterRewards.connect(otherAccount).setGalaxyMember(otherAccount.address)).to.be.reverted; }); it("Should not be able to register vote if proposal start is zero", async () => { const { voterRewards, otherAccount, owner } = await getOrDeployContractInstances({ forceDeploy: true, }); await voterRewards.connect(owner).grantRole(await voterRewards.VOTE_REGISTRAR_ROLE(), otherAccount.address); await expect(voterRewards .connect(otherAccount) .registerVote(0, otherAccount.address, ethers.parseEther("1000"), ethers.parseEther(Math.sqrt(1000).toString()))).to.be.reverted; }); it("Should revert if admin is set to zero address in initilisation", async () => { const config = createLocalConfig(); const { owner, b3tr, galaxyMember, emissions } = await getOrDeployContractInstances({ forceDeploy: true, config, }); await expect(deployProxy("VoterRewardsV1", [ ZERO_ADDRESS, // admin owner.address, // upgrader owner.address, // contractsAddressManager await emissions.getAddress(), await galaxyMember.getAddress(), await b3tr.getAddress(), levels, multipliers, ])).to.be.reverted; }); it("Should not be able to register vote for zero address voter", async () => { const { voterRewards, otherAccount, owner } = await getOrDeployContractInstances({ forceDeploy: true, }); await voterRewards.connect(owner).grantRole(await voterRewards.VOTE_REGISTRAR_ROLE(), otherAccount.address); await expect(voterRewards .connect(otherAccount) .registerVote(1, ZERO_ADDRESS, ethers.parseEther("1000"), ethers.parseEther(Math.sqrt(1000).toString()))).to.be.reverted; }); it("Should return correct scaling factor", async () => { const { voterRewards } = await getOrDeployContractInstances({ forceDeploy: true }); expect(await voterRewards.SCALING_FACTOR()).to.equal(10 ** 6); }); it("Should return correct b3tr address", async () => { const { voterRewards, b3tr } = await getOrDeployContractInstances({ forceDeploy: true }); expect(await voterRewards.b3tr()).to.equal(await b3tr.getAddress()); }); it("Should be able to set level to multiplier", async () => { const { voterRewards, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await voterRewards.connect(owner).setLevelToMultiplierNow(1, 2); expect(await voterRewards.levelToMultiplier(1)).to.equal(2); await expect(voterRewards.connect(owner).setLevelToMultiplierNow(0, 2)).to.be.reverted; // Level cannot be zero await expect(voterRewards.connect(owner).setLevelToMultiplierNow(1, 0)).to.be.reverted; // Multiplier cannot be zero await expect(voterRewards.connect(otherAccount).setLevelToMultiplierNow(1, 2)).to.be.reverted; // Should not be able to set level to multiplier if not admin }); it("Should be able to set galaxy member address", async () => { const { voterRewards, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await voterRewards.connect(owner).setGalaxyMember(otherAccount.address); expect(await voterRewards.galaxyMember()).to.equal(otherAccount.address); await expect(voterRewards.connect(otherAccount).setGalaxyMember(otherAccount.address)).to.be.reverted; // Should not be able to set galaxy member address if not admin await expect(voterRewards.connect(owner).setGalaxyMember(ZERO_ADDRESS)).to.be.reverted; // Galaxy member address cannot be zero }); it("Should be able to set emissions address", async () => { const { voterRewards, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await voterRewards.connect(owner).setEmissions(otherAccount.address); expect(await voterRewards.emissions()).to.equal(otherAccount.address); await expect(voterRewards.connect(otherAccount).setEmissions(otherAccount.address)).to.be.reverted; // Should not be able to set emissions address if not admin await expect(voterRewards.connect(owner).setEmissions(ZERO_ADDRESS)).to.be.reverted; // Emissions address cannot be zero }); it("Admin should be able to set vote registrar role address", async () => { const { voterRewards, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await voterRewards.connect(owner).grantRole(await voterRewards.VOTE_REGISTRAR_ROLE(), otherAccount.address); }); it(" admin should be able to set vote registrar role address", async () => { const { voterRewards, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); expect(await voterRewards.hasRole(await voterRewards.VOTE_REGISTRAR_ROLE(), otherAccount.address)).to.eql(false); await expect(voterRewards.connect(otherAccount).grantRole(await voterRewards.VOTE_REGISTRAR_ROLE(), otherAccount.address)).to.be.reverted; }); it("Should be able to disable Quadratic Rewards", async () => { const { voterRewards, owner } = await getOrDeployContractInstances({ forceDeploy: true }); expect(await voterRewards.isQuadraticRewardingDisabledAtBlock(await ethers.provider.getBlockNumber())).to.eql(false); const tx = await voterRewards.connect(owner).toggleQuadraticRewarding(); const receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); const events = receipt?.logs; const decodedEvents = events?.map(event => { return voterRewards.interface.parseLog({ topics: event?.topics, data: event?.data, }); }); const event = decodedEvents.find(event => event?.name === "QuadraticRewardingToggled"); expect(event).to.not.equal(undefined); expect(await voterRewards.isQuadraticRewardingDisabledAtBlock(await ethers.provider.getBlockNumber())).to.eql(true); }); it("Quadratic Rewards should be enabled by default", async () => { const { voterRewards } = await getOrDeployContractInstances({ forceDeploy: true }); expect(await voterRewards.isQuadraticRewardingDisabledAtBlock(1)).to.eql(false); }); it("Only admin should be able to disable Quadratic Rewards", async () => { const { voterRewards, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true }); await expect(voterRewards.connect(otherAccount).toggleQuadraticRewarding()).to.be.reverted; }); it("Clock should return correct block number", async () => { const { voterRewards } = await getOrDeployContractInstances({ forceDeploy: true }); expect(await voterRewards.clock()).to.equal(await ethers.provider.getBlockNumber()); }); }); describe("Contract upgradeablity", () => { it("Admin should be able to upgrade the contract", async function () { const { voterRewards, owner } = await getOrDeployContractInstances({ forceDeploy: true, }); // Deploy the implementation contract const Contract = await ethers.getContractFactory("VoterRewards"); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); const currentImplAddress = await getImplementationAddress(ethers.provider, await voterRewards.getAddress()); const UPGRADER_ROLE = await voterRewards.UPGRADER_ROLE(); expect(await voterRewards.hasRole(UPGRADER_ROLE, owner.address)).to.eql(true); await expect(voterRewards.connect(owner).upgradeToAndCall(await implementation.getAddress(), "0x")).to.not.be .reverted; const newImplAddress = await getImplementationAddress(ethers.provider, await voterRewards.getAddress()); expect(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase()); expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()); }); it("Admin should be able to upgrade the contract", async function () { const { voterRewards, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true, }); // Deploy the implementation contract const Contract = await ethers.getContractFactory("VoterRewards"); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); const currentImplAddress = await getImplementationAddress(ethers.provider, await voterRewards.getAddress()); const UPGRADER_ROLE = await voterRewards.UPGRADER_ROLE(); expect(await voterRewards.hasRole(UPGRADER_ROLE, otherAccount.address)).to.eql(false); await expect(voterRewards.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")).to.be .reverted; const newImplAddress = await getImplementationAddress(ethers.provider, await voterRewards.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 { voterRewards, owner, otherAccount } = await getOrDeployContractInstances({ forceDeploy: true, }); // Deploy the implementation contract const Contract = await ethers.getContractFactory("VoterRewards"); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); const currentImplAddress = await getImplementationAddress(ethers.provider, await voterRewards.getAddress()); const UPGRADER_ROLE = await voterRewards.UPGRADER_ROLE(); expect(await voterRewards.hasRole(UPGRADER_ROLE, otherAccount.address)).to.eql(false); await expect(voterRewards.connect(owner).grantRole(UPGRADER_ROLE, otherAccount.address)).to.not.be.reverted; await expect(voterRewards.connect(owner).revokeRole(UPGRADER_ROLE, owner.address)).to.not.be.reverted; await expect(voterRewards.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")).to.not .be.reverted; const newImplAddress = await getImplementationAddress(ethers.provider, await voterRewards.getAddress()); expect(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase()); expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()); }); it("Should not be able to initialize the contract after already being initialized", async function () { const { voterRewards, owner, emissions, galaxyMember, b3tr } = await getOrDeployContractInstances({ forceDeploy: true, }); await expect(voterRewards.initialize(owner.address, owner.address, owner.address, await emissions.getAddress(), await galaxyMember.getAddress(), await b3tr.getAddress(), levels, multipliers)).to.be.reverted; }); it("Should not be able to deploy proxy with galaxy member address as zero address", async function () { const { owner, emissions, b3tr } = await getOrDeployContractInstances({ forceDeploy: true, }); await expect(deployProxy("VoterRewardsV1", [ owner.address, owner.address, owner.address, await emissions.getAddress(), ZERO_ADDRESS, await b3tr.getAddress(), levels, multipliers, ])).to.be.reverted; }); it("Should not be able to deploy proxy with emissions address as zero address", async function () { const { owner, galaxyMember, b3tr } = await getOrDeployContractInstances({ forceDeploy: true, }); await expect(deployProxy("VoterRewardsV1", [ owner.address, owner.address, owner.address, ZERO_ADDRESS, await galaxyMember.getAddress(), await b3tr.getAddress(), levels, multipliers, ])).to.be.reverted; }); it("Should not be able to deploy proxy with b3tr address as zero address", async function () { const { owner, emissions, galaxyMember } = await getOrDeployContractInstances({ forceDeploy: true, }); await expect(deployProxy("VoterRewardsV1", [ owner.address, owner.address, owner.address, await emissions.getAddress(), await galaxyMember.getAddress(), ZERO_ADDRESS, levels, multipliers, ])).to.be.reverted; }); it("Should not be able to deploy proxy with incorrect levels and multipliers", async function () { const { owner, emissions, galaxyMember, b3tr } = await getOrDeployContractInstances({ forceDeploy: true, }); await expect(deployProxy("VoterRewardsV1", [ owner.address, owner.address, owner.address, await emissions.getAddress(), await galaxyMember.getAddress(), await b3tr.getAddress(), [1, 2, 3, 4, 5, 6, 7, 8, 9, 10], [1, 2, 3, 4, 5, 6, 7, 8, 9], // Incorrect multipliers length should be same as levels length ])).to.be.reverted; }); it("Should not be able to deploy proxy with levels empty", async function () { const { owner, emissions, galaxyMember, b3tr } = await getOrDeployContractInstances({ forceDeploy: true, }); await expect(deployProxy("VoterRewardsV1", [ owner.address, owner.address, owner.address, await emissions.getAddress(), await galaxyMember.getAddress(), await b3tr.getAddress(), [], [], ])).to.be.reverted; }); it("Should return correct version of the contract", async () => { const { voterRewards } = await getOrDeployContractInstances({ forceDeploy: true, }); expect(await voterRewards.version()).to.equal("7"); }); }); describe("X Allocation voting rewards", () => { it("Should track voting rewards correctly involving multiple voters", async () => { const config = createLocalConfig(); const { xAllocationVoting, otherAccounts, otherAccount, xAllocationPool, owner, voterRewards, emissions, b3tr, minterAccount, x2EarnApps, veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, config, }); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const app1 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)); await endorseApp(app1, otherAccounts[0]); await x2EarnApps .connect(creator1) .submitApp(otherAccounts[1].address, otherAccounts[1].address, otherAccounts[1].address, "metadataURI"); const app2 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[1].address)); await endorseApp(app2, otherAccounts[1]); const voter2 = otherAccounts[3]; const voter3 = otherAccounts[4]; await veBetterPassport.whitelist(otherAccount.address); await veBetterPassport.whitelist(voter2.address); await veBetterPassport.whitelist(voter3.address); await veBetterPassport.toggleCheck(1); await getVot3Tokens(otherAccount, "1000"); await getVot3Tokens(voter2, "1000"); await getVot3Tokens(voter3, "1000"); // Bootstrap emissions await bootstrapEmissions(); let tx = await emissions.connect(minterAccount).start(); let receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); let events = receipt?.logs; let decodedEvents = events?.map(event => { return xAllocationVoting.interface.parseLog({ topics: event?.topics, data: event?.data, }); }); const proposalEvent = decodedEvents.find(event => event?.name === "RoundCreated"); expect(proposalEvent).to.not.equal(undefined); expect(await emissions.getCurrentCycle()).to.equal(1); expect(await b3tr.balanceOf(await xAllocationPool.getAddress())).to.equal(config.INITIAL_X_ALLOCATION); expect(await emissions.nextCycle()).to.equal(2); await waitForNextCycle(); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); expect(roundId).to.equal(2); expect(await xAllocationVoting.roundDeadline(roundId)).to.lt(await emissions.getNextCycleBlock()); tx = await xAllocationVoting .connect(otherAccount) .castVote(roundId, [app1, app2], [ethers.parseEther("300"), ethers.parseEther("200")]); receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); events = receipt?.logs; decodedEvents = events ?.map(event => { return voterRewards.interface.parseLog({ topics: event?.topics, data: event?.data, }); }) .filter(e => e?.name === "VoteRegistered"); expect(decodedEvents[0]?.args?.[0]).to.equal(2); // Cycle expect(decodedEvents[0]?.args?.[1]).to.equal(otherAccount.address); // Voter expect(await emissions.isCycleEnded(roundId)).to.equal(false); await catchRevert(voterRewards.claimReward(roundId, otherAccount.address)); // Should not be able to claim rewards before cycle ended expect(await voterRewards.cycleToVoterToTotal(roundId, otherAccount)).to.equal(ethers.parseEther("22.360679774")); // I'm expecting 22.36 because I voted 300 for app1 and 200 for app2 at the first cycle which is 500 and the square root of 500 is 22.36 tx = await xAllocationVoting .connect(voter2) .castVote(roundId, [app1, app2], [ethers.parseEther("200"), ethers.parseEther("100")]); receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); expect(await voterRewards.cycleToVoterToTotal(roundId, voter2)).to.equal(ethers.parseEther("17.320508075")); // I'm expecting 17.32 because I voted 200 for app1 and 100 for app2 at the first cycle which is 300 and the square root of 300 is 17.32 await catchRevert(voterRewards.claimReward(roundId, voter2.address)); // Should not be able to claim rewards before cycle ended tx = await xAllocationVoting .connect(voter3) .castVote(roundId, [app1, app2], [ethers.parseEther("100"), ethers.parseEther("500")]); receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); expect(await voterRewards.cycleToVoterToTotal(roundId, voter3)).to.equal(ethers.parseEther("24.494897427")); // I'm expecting 24.49 because I voted 100 for app1 and 500 for app2 at the first cycle which is 600 and the square root of 600 is 24.49 // Votes should be tracked correctly let appVotes = await xAllocationVoting.getAppVotes(roundId, app1); expect(appVotes).to.eql(ethers.parseEther("600")); appVotes = await xAllocationVoting.getAppVotes(roundId, app2); expect(appVotes).to.eql(ethers.parseEther("800")); let totalVotes = await xAllocationVoting.totalVotes(roundId); expect(totalVotes).to.eql(ethers.parseEther("1400")); // Total voters should be tracked correctly const totalVoters = await xAllocationVoting.totalVoters(roundId); expect(totalVoters).to.eql(BigInt(3)); // Voter rewards checks expect(await voterRewards.cycleToTotal(roundId)).to.equal(ethers.parseEther("64.176085276")); // Total votes -> Math.sqrt(500) + Math.sqrt(300) + Math.sqrt(600) expect(await voterRewards.cycleToTotal(roundId)).to.equal((await voterRewards.cycleToVoterToTotal(roundId, otherAccount)) + (await voterRewards.cycleToVoterToTotal(roundId, voter2)) + (await voterRewards.cycleToVoterToTotal(roundId, voter3))); // Total votes await waitForRoundToEnd(Number(roundId)); // Votes should be the same after round ended appVotes = await xAllocationVoting.getAppVotes(roundId, app1); expect(appVotes).to.eql(ethers.parseEther("600")); appVotes = await xAllocationVoting.getAppVotes(roundId, app2); expect(appVotes).to.eql(ethers.parseEther("800")); totalVotes = await xAllocationVoting.totalVotes(roundId); expect(totalVotes).to.eql(ethers.parseEther("1400")); await waitForNextCycle(); expect(await emissions.isCycleDistributed(await emissions.nextCycle())).to.equal(false); expect(await emissions.isNextCycleDistributable()).to.equal(true); // Reward claiming expect(await emissions.isCycleDistributed(1)).to.equal(true); expect(await b3tr.balanceOf(await voterRewards.getAddress())).to.equal((await emissions.getVote2EarnAmount(1)) + (await emissions.getVote2EarnAmount(2)) + (await emissions.getGMAmount(2))); const voter1Rewards = await voterRewards.getReward(roundId, otherAccount.address); const voter2Rewards = await voterRewards.getReward(roundId, voter2.address); const voter3Rewards = await voterRewards.getReward(roundId, voter3.address); tx = await voterRewards.connect(otherAccount).claimReward(roundId, otherAccount.address); receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); expect(await b3tr.balanceOf(otherAccount.address)).to.equal(voter1Rewards); events = receipt?.logs; decodedEvents = events?.map(event => { return voterRewards.interface.parseLog({ topics: event?.topics, data: event?.data, }); }); const rewardClaimedEvent = decodedEvents.find(event => event?.name === "RewardClaimedV2"); expect(rewardClaimedEvent?.args?.[0]).to.equal(roundId); // Cycle expect(rewardClaimedEvent?.args?.[1]).to.equal(otherAccount.address); // Voter expect(rewardClaimedEvent?.args?.[2]).to.equal(696853966016598011228309n); // Reward await voterRewards.connect(voter2).claimReward(roundId, voter2.address); await voterRewards.connect(voter3).claimReward(roundId, voter3.address); await expect(voterRewards.connect(voter2).claimReward(1, ZERO_ADDRESS)).to.be.reverted; // Should not be able to claim rewards for zero address expect(await b3tr.balanceOf(voter2.address)).to.equal(voter2Rewards); expect(await b3tr.balanceOf(voter3.address)).to.equal(voter3Rewards); expect(await b3tr.balanceOf(await voterRewards.getAddress())).to.lt(ethers.parseEther("22500001")); // Round 1 + GM pool round 2 }); it("Should track voting rewards correctly involving multiple voters when Quadratic Rewarding is disabled", async () => { const config = createLocalConfig(); const { xAllocationVoting, otherAccounts, otherAccount, xAllocationPool, owner, voterRewards, emissions, b3tr, minterAccount, x2EarnApps, veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }); await voterRewards.connect(owner).toggleQuadraticRewarding(); expect(await voterRewards.isQuadraticRewardingDisabledAtBlock(await ethers.provider.getBlockNumber())).to.eql(true); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const app1 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)); await endorseApp(app1, otherAccounts[0]); await x2EarnApps .connect(creator1) .submitApp(otherAccounts[1].address, otherAccounts[1].address, otherAccounts[1].address, "metadataURI"); const app2 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[1].address)); await endorseApp(app2, otherAccounts[1]); const voter2 = otherAccounts[3]; const voter3 = otherAccounts[4]; await veBetterPassport.whitelist(otherAccount.address); await veBetterPassport.whitelist(voter2.address); await veBetterPassport.whitelist(voter3.address); await veBetterPassport.toggleCheck(1); await getVot3Tokens(otherAccount, "1000"); await getVot3Tokens(voter2, "1000"); await getVot3Tokens(voter3, "1000"); // Bootstrap emissions await bootstrapEmissions(); let tx = await emissions.connect(minterAccount).start(); let receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); let events = receipt?.logs; let decodedEvents = events?.map(event => { return xAllocationVoting.interface.parseLog({ topics: event?.topics, data: event?.data, }); }); const proposalEvent = decodedEvents.find(event => event?.name === "RoundCreated"); expect(proposalEvent).to.not.equal(undefined); expect(await emissions.getCurrentCycle()).to.equal(1); expect(await b3tr.balanceOf(await xAllocationPool.getAddress())).to.equal(config.INITIAL_X_ALLOCATION); expect(await emissions.nextCycle()).to.equal(2); const roundId = await xAllocationVoting.currentRoundId(); expect(roundId).to.equal(1); expect(await xAllocationVoting.roundDeadline(roundId)).to.lt(await emissions.getNextCycleBlock()); tx = await xAllocationVoting .connect(otherAccount) .castVote(roundId, [app1, app2], [ethers.parseEther("300"), ethers.parseEther("200")]); receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); events = receipt?.logs; decodedEvents = events ?.map(event => { return voterRewards.interface.parseLog({ topics: event?.topics, data: event?.data, }); }) .filter(e => e?.name === "VoteRegistered"); expect(decodedEvents[0]?.args?.[0]).to.equal(1); // Cycle expect(decodedEvents[0]?.args?.[1]).to.equal(otherAccount.address); // Voter expect(decodedEvents[0]?.args?.[2]).to.equal(ethers.parseEther("500")); // Votes expect(decodedEvents[0]?.args?.[3]).to.equal(ethers.parseEther("500")); // Reward weight expect(await emissions.isCycleEnded(1)).to.equal(false); await catchRevert(voterRewards.claimReward(1, otherAccount.address)); // Should not be able to claim rewards before cycle ended expect(await voterRewards.cycleToVoterToTotal(1, otherAccount)).to.equal(ethers.parseEther("500")); // I'm expecting 500 because I voted 300 for app1 and 200 for app2 at the first cycle which is 500 tx = await xAllocationVoting .connect(voter2) .castVote(roundId, [app1, app2], [ethers.parseEther("200"), ethers.parseEther("100")]); receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); expect(await voterRewards.cycleToVoterToTotal(1, voter2)).to.equal(ethers.parseEther("300")); // I'm expecting 300 because I voted 200 for app1 and 100 for app2 at the first cycle which is 300 await catchRevert(voterRewards.claimReward(1, voter2.address)); // Should not be able to claim rewards before cycle ended tx = await xAllocationVoting .connect(voter3) .castVote(roundId, [app1, app2], [ethers.parseEther("100"), ethers.parseEther("500")]); receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); expect(await voterRewards.cycleToVoterToTotal(1, voter3)).to.equal(ethers.parseEther("600")); // I'm expecting 600 because I voted 100 for app1 and 500 for app2 at the first cycle which is 600 // Votes should be tracked correctly let appVotes = await xAllocationVoting.getAppVotes(roundId, app1); expect(appVotes).to.eql(ethers.parseEther("600")); appVotes = await xAllocationVoting.getAppVotes(roundId, app2); expect(appVotes).to.eql(ethers.parseEther("800")); let totalVotes = await xAllocationVoting.totalVotes(roundId); expect(totalVotes).to.eql(ethers.parseEther("1400")); // Total voters should be tracked correctly const totalVoters = await xAllocationVoting.totalVoters(roundId); expect(totalVoters).to.eql(BigInt(3)); // Voter rewards checks expect(await voterRewards.cycleToTotal(1)).to.equal(ethers.parseEther("1400")); // Total votes expect(await voterRewards.cycleToTotal(1)).to.equal((await voterRewards.cycleToVoterToTotal(1, otherAccount)) + (await voterRewards.cycleToVoterToTotal(1, voter2)) + (await voterRewards.cycleToVoterToTotal(1, voter3))); // Total votes await waitForRoundToEnd(Number(roundId)); // Votes should be the same after round ended appVotes = await xAllocationVoting.getAppVotes(roundId, app1); expect(appVotes).to.eql(ethers.parseEther("600")); appVotes = await xAllocationVoting.getAppVotes(roundId, app2); expect(appVotes).to.eql(ethers.parseEther("800")); totalVotes = await xAllocationVoting.totalVotes(roundId); expect(totalVotes).to.eql(ethers.parseEther("1400")); await waitForNextCycle(); expect(await emissions.isCycleDistributed(await emissions.nextCycle())).to.equal(false); expect(await emissions.isNextCycleDistributable()).to.equal(true); // Reward claiming expect(await emissions.isCycleDistributed(1)).to.equal(true); expect(await b3tr.balanceOf(await voterRewards.getAddress())).to.equal(await emissions.getVote2EarnAmount(1)); const voter1Rewards = await voterRewards.getReward(1, otherAccount.address); const voter2Rewards = await voterRewards.getReward(1, voter2.address); const voter3Rewards = await voterRewards.getReward(1, voter3.address); tx = await voterRewards.connect(otherAccount).claimReward(1, otherAccount); receipt = await tx.wait(); if (!receipt) throw new Error("No receipt"); expect(await b3tr.balanceOf(otherAccount.address)).to.equal(voter1Rewards); events = receipt?.logs; decodedEvents = events?.map(event => { return voterRewards.interface.parseLog({ topics: event?.topics, data: event?.data, }); }); const rewardClaimedEvent = decodedEvents.find(event => event?.name === "RewardClaimedV2"); expect(rewardClaimedEvent?.args?.[0]).to.equal(1); // Cycle expect(rewardClaimedEvent?.args?.[1]).to.equal(otherAccount.address); // Voter expect(rewardClaimedEvent?.args?.[2]).to.equal(714285714285714285714285n); // Reward await voterRewards.connect(voter2).claimReward(1, voter2.address); await voterRewards.connect(voter3).claimReward(1, voter3.address); await expect(voterRewards.connect(voter2).claimReward(1, ZERO_ADDRESS)).to.be.reverted; // Should not be able to claim rewards for zero address expect(await b3tr.balanceOf(voter2.address)).to.equal(voter2Rewards); expect(await b3tr.balanceOf(voter3.address)).to.equal(voter3Rewards); expect(await b3tr.balanceOf(await voterRewards.getAddress())).to.lt(ethers.parseEther("1")); }); it("Should track voting rewards correctly involving multiple voters and multiple rounds", async () => { const { xAllocationVoting, otherAccounts, otherAccount: voter1, owner, voterRewards, emissions, b3tr, minterAccount, x2EarnApps, veBetterPassport, } = await getOrDeployContractInstances({ forceDeploy: true, }); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); const app1 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[0].address)); await endorseApp(app1, otherAccounts[0]); await x2EarnApps .connect(creator1) .submitApp(otherAccounts[1].address, otherAccounts[1].address, otherAccounts[1].address, "metadataURI"); const app2 = ethers.keccak256(ethers.toUtf8Bytes(otherAccounts[1].address)); await endorseApp(app2, otherAccounts[1]); const voter2 = otherAccounts[3]; const voter3 = otherAccounts[4]; await veBetterPassport.whitelist(voter1.address); await veBetterPassport.whitelist(voter2.address); await veBetterPassport.whitelist(voter3.address); await veBetterPassport.toggleCheck(1); await getVot3Tokens(voter1, "1000"); await getVot3Tokens(voter2, "1000"); await getVot3Tokens(voter3, "1000"); // Bootstrap emissions await bootstrapEmissions(); await emissions.connect(minterAccount).start(); const roundId = await xAllocationVoting.currentRoundId(); const isdisabled = await voterRewards.isQuadraticRewardingDisabledForCurrentCycle(); expect(isdisabled).to.equal(false); expect(roundId).to.equal(1); expect(await xAllocationVoting.roundDeadline(roundId)).to.lt(await emissions.getNextCycleBlock()); // Vote on apps for the first round await voteOnApps([app1, app2], [voter1, voter2, voter3], [ [ethers.parseEther("1000"), ethers.parseEther("0")], // Voter 1 votes 1000 for app1 [ethers.parseEther("200"), ethers.parseEther("100")], // Voter 2 votes 200 for app1 and 100 for app2 [ethers.parseEther("500"), ethers.parseEther("500")], // Voter 3 votes 500 for app1 and 500 for app2 ], roundId); expect(await emissions.isCycleEnded(1)).to.equal(false); await catchRevert(voterRewards.claimReward(1, voter1.address)); expect(await voterRewards.cycleToVoterToTotal(1, voter1)).to.equal(ethers.parseEther("31.622776601")); expect(await voterRewards.cycleToVoterToTotal(1, voter2)).to.equal(ethers.parseEther("17.320508075")); await catchRevert(voterRewards.claimReward(1, voter2.address)); expect(await voterRewards.cycleToVoterToTotal(1, voter3)).to.equal(ethers.parseEther("31.622776601")); // Votes should be tracked correctly let appVotes = await xAllocationVoting.getAppVotes(roundId, app1); expect(appVotes).to.eql(ethers.parseEther("1700")); appVotes = await xAllocationVoting.getAppVotes(roundId, app2); expect(appVotes).to.eql(ethers.parseEther("600")); let totalVotes = await xAllocationVoting.totalVotes(roundId); expect(totalVotes).to.eql(ethers.parseEther("2300")); // Total voters should be tracked correctly let totalVoters = await xAllocationVoting.totalVoters(roundId); expect(totalVoters).to.eql(BigInt(3)); // Voter rewards checks expect(await voterRewards.cycleToTotal(1)).to.equal(ethers.parseEther("80.566061277")); // Total votes -> Math.sqrt(1000) + Math.sqrt(300) + Math.sqrt(1000) expect(await voterRewards.cycleToTotal(1)).to.equal((await voterRewards.cycleToVoterToTotal(1, voter1)) + (await voterRewards.cycleToVoterToTotal(1, voter2)) + (await voterRewards.cycleToVoterToTotal(1, voter3))); // Total votes await waitForRoundToEnd(Number(roundId)); // Votes should be the same after round ended appVotes = await xAllocationVoting.getAppVotes(roundId, app1); expect(appVotes).to.eql(ethers.parseEther("1700")); appVotes = await xAllocationVoting.getAppVotes(roundId, app2); expect(appVotes).to.eql(ethers.parseEther("600")); totalVotes = await xAllocationVoting.totalVotes(roundId); expect(totalVotes).to.eql(ethers.parseEther("2300")); await waitForNextCycle(); expect(await emissions.isCycleDistributed(await emissions.nextCycle())).to.equal(false); expect(await emissions.isNextCycleDistributable()).to.equal(true); // Reward claiming expect(await emissions.isCycleDistributed(1)).to.equal(true); expect(await b3tr.balanceOf(await voterRewards.getAddress())).to.equal(await emissions.getVote2EarnAmount(1)); const voter1Rewards = await voterRewards.getReward(1, voter1.address); const voter2Rewards = await voterRewards.getReward(1, voter2.address); const voter3Rewards = await voterRewards.getReward(1, voter3.address); await voterRewards.connect(voter1).claimReward(1, voter1); expect(await b3tr.balanceOf(voter1.address)).to.equal(voter1Rewards); expect(await b3tr.balanceOf(await voterRewards.getAddress())).to.equal((await emissions.getVote2EarnAmount(1)) - voter1Rewards); // Second round await emissions.connect(voter1).distribute(); // Anyone can distribute the cycle const roundId2 = await xAllocationVoting.currentRoundId(); expect(roundId2).to.equal(2); expect(await xAllocationVoting.roundDeadline(roundId)).to.lt(await emissions.getNextCycleBlock()); // Vote on apps for the second round await voteOnApps([app1, app2], [voter1, voter2, voter3], [ [ethers.parseEther("0"), ethers.parseEther("1000")], // Voter 1 votes 1000 for app2 [ethers.parseEther("100"), ethers.parseEther("500")], // Voter 2 votes 100 for app1 and 500 for app2 [ethers.parseEther("500"), ethers.parseEther("500")], // Voter 3 votes 500 for app1 and 500 for app2 ], roundId2); expect(await emissions.isCycleEnded(2)).to.equal(false); await catchRevert(voterRewards.claimReward(2, voter1.address)); expect(await voterRewards.cycleToVoterToTotal(2, voter1)).to.equal(ethers.parseEther("31.622776601")); expect(await voterRewards.cycleToVoterToTotal(2, voter2)).to.equal(ethers.parseEther("24.494897427")); await catchRevert(voterRewards.claimReward(2, voter2.address)); expect(await voterRewards.cycleToVoterToTotal(2, voter3)).to.equal(ethers.parseEther("31.622776601")); // Votes should be tracked correctly appVotes = await xAllocationVoting.getAppVotes(roundId2, app1); expect(appVotes).to.eql(ethers.parseEther("600")); appVotes = await xAllocationVoting.getAppVotes(roundId2, app2); expect(appVotes).to.eql(ethers.parseEther("2000")); totalVotes = await xAllocationVoting.totalVotes(roundId2); expect(totalVotes).to.eql(ethers.parseEther("2600")); // Total voters should be tracked correctly totalVoters = await xAllocationVoting.totalVoters(roundId2); expect(totalVoters).to.eql(BigInt(3)); // Voter rewards checks expect(await voterRewards.cycleToTotal(2)).to.equal(ethers.parseEther("87.740450629")); // Total votes -> Math.sqrt(1000) + Math.sqrt(300) + Math.sqrt(1000) expect(await voterRewards.cycleToTotal(2)).to.equal((await voterRewards.cycleToVoterToTotal(2, voter1)) + (await voterRewards.cycleToVoterToTotal(2, voter2)) + (await voterRewards.cycleToVoterToTotal(2, voter3))); // Total votes await waitForRoundToEnd(Number(roundId2)); // Votes should be the same after round ended appVotes = await xAllocationVoting.getAppVotes(roundId2, app1); expect(appVotes).to.eql(ethers.parseEther("600")); appVotes = await xAllocationVoting.getAppVotes(roundId2, app2); expect(appVotes).to.eql(ethers.parseEther("2000")); totalVotes = await xAllocationVoting.totalVotes(roundId2); expect(totalVotes).to.eql(ethers.parseEther("2600")); await waitForNextCycle(); expect(await emissions.isCycleEnded(2)).to.equal(true); expect(await emissions.isCycleDistributed(await emissions.nextCycle())).to.equal(false); expect(await emissions.isNextCycleDistributable()).to.equal(true); // Reward claiming expect(await emissions.isCycleDistributed(2)).to.equal(true); expect(await b3tr.balanceOf(await voterRewards.getAddress())).to.gt(await emissions.getVote2EarnAmount(2)); // Voters of round 1 can still claim rewards of round 1 thus the balance of VoterRewards contract should be greater than the emission amount const voter1Rewards2 = await voterRewards.getReward(2, voter1.address); const voter2Rewards2 = await voterRewards.getReward(2, voter2.address); const voter3Rewards2 = await voterRewards.getReward(2, voter3.address); await voterRewards.connect(voter1).claimReward(2, voter1); await voterRewards.connect(voter2).claimReward(2, voter2); await voterRewards.connect(voter3).claimReward(2, voter3); expect(await b3tr.balanceOf(voter1.address)).to.equal(voter1Rewards + voter1Rewards2); // Voter 1 claimed also rewards of round 1 expect(await b3tr.balanceOf(voter2.address)).to.equal(voter2Rewards2); expect(await b3tr.balanceOf(voter3.address)).to.equal(voter3Rewards2); // Voters of round 1 can still claim rewards of round 1 await voterRewards.connect(voter2).claimReward(1, voter2); await voterRewards.connect(voter3).claimReward(1, voter3); expect(await b3tr.balanceOf(voter2.address)).to.equal(voter2Rewards + voter2Rewards2); expect(await b3tr.balanceOf(voter3.address)).to.equal(voter3Rewards + voter3Rewards2); }); it("Should increase GM voting rewards for user's with higher token levels", async () => { const config = createTestConfig(); const { xAllocationVoting, otherAccounts, otherAccount: voter1, owner, voterRewards, emissions, b3tr, minterAccount, governor, treasury, veBetterPassport, x2EarnApps, nodeManagement, vechainNodesMock, } = await getOrDeployContractInstances({ forceDeploy: true, }); const galaxyMemberV1 = (await deployProxy("GalaxyMemberV1", [ { name: "galaxyMember", symbol: "GM", admin: owner.address, upgrader: owner.address, pauser: owner.address, minter: owner.address, contractsAddressManager: owner.address, maxLevel: 10, baseTokenURI: config.GM_NFT_BASE_URI, b3trToUpgradeToLevel: config.GM_NFT_B3TR_REQUIRED_TO_UPGRADE_TO_LEVEL, b3tr: await b3tr.getAddress(), treasury: await treasury.getAddress(), }, ])); const galaxyMember = (await upgradeProxy("GalaxyMemberV1", "GalaxyMemberV2", await galaxyMemberV1.getAddress(), [ await vechainNodesMock.getAddress(), await nodeManagement.getAddress(), owner.address, config.GM_NFT_NODE_TO_FREE_LEVEL, ], { version: 2 })); await galaxyMember.waitForDeployment(); await galaxyMember.connect(owner).setB3trGovernorAddress(await governor.getAddress()); await galaxyMember.connect(owner).setXAllocationsGovernorAddress(await xAllocationVoting.getAddress()); await voterRewards.setGalaxyMember(await galaxyMember.getAddress()); await x2EarnApps