@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
JavaScript
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