@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
1,092 lines • 71.6 kB
JavaScript
const { ethers } = require("hardhat");
import { expect } from "chai";
import { describe, it } from "mocha";
import { deployProxy, deployAndUpgrade, upgradeProxy } from "../../scripts/helpers/upgrades";
import { getOrDeployContractInstances, levels, multipliers } from "../helpers/deploy";
import { createLocalConfig } from "@repo/config/contracts/envs/local";
import { getVot3Tokens, waitForRoundToEnd, waitForNextCycle, bootstrapAndStartEmissions, voteOnApps, catchRevert, moveBlocks, upgradeNFTtoLevel, waitForNextBlock, } from "../helpers";
import { endorseApp } from "../helpers/xnodes";
describe("VoterRewards Upgrade Test - @shard10a", function () {
let creator1;
before(async function () {
const config = await getOrDeployContractInstances({ forceDeploy: true });
if (!config)
throw new Error("Failed to deploy contracts");
creator1 = config.creators[0];
});
it("Should not have state conflict after upgrading to V3 and V4", async () => {
const config = createLocalConfig();
const contractConfig = await getOrDeployContractInstances({
forceDeploy: true,
});
if (!contractConfig)
throw new Error("Failed to deploy contracts");
const { otherAccounts, otherAccount: voter1, owner, emissions, b3tr, timeLock, vechainNodesMock, nodeManagement, vot3, treasury, x2EarnApps, xAllocationPool, governorClockLogicLibV1, governorConfiguratorLibV1, governorDepositLogicLibV1, governorFunctionRestrictionsLogicLibV1, governorProposalLogicLibV1, governorQuorumLogicLibV1, governorStateLogicLibV1, governorVotesLogicLibV1, governorClockLogicLibV3, governorConfiguratorLibV3, governorDepositLogicLibV3, governorFunctionRestrictionsLogicLibV3, governorProposalLogicLibV3, governorQuorumLogicLibV3, governorStateLogicLibV3, governorVotesLogicLibV3, governorClockLogicLibV4, governorConfiguratorLibV4, governorDepositLogicLibV4, governorFunctionRestrictionsLogicLibV4, governorProposalLogicLibV4, governorQuorumLogicLibV4, governorStateLogicLibV4, governorVotesLogicLibV4, veBetterPassport, } = contractConfig;
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: 1,
baseTokenURI: config.GM_NFT_BASE_URI,
xNodeMaxMintableLevels: [1, 2, 3, 4, 5, 6, 7],
b3trToUpgradeToLevel: [1000000n],
b3tr: await b3tr.getAddress(),
treasury: await treasury.getAddress(),
},
]));
const voterRewardsV1 = (await deployProxy("VoterRewardsV1", [
owner.address, // admin
owner.address, // upgrader
owner.address, // contractsAddressManager
await emissions.getAddress(),
await galaxyMemberV1.getAddress(),
await b3tr.getAddress(),
levels,
multipliers,
]));
// Deploy XAllocationVoting
const xAllocationVotingV2 = (await deployAndUpgrade(["XAllocationVotingV1", "XAllocationVotingV2"], [
[
{
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 voterRewardsV1.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],
}));
// Deploy Governor
const governorV4 = (await deployAndUpgrade(["B3TRGovernorV1", "B3TRGovernorV2", "B3TRGovernorV3", "B3TRGovernorV4"], [
[
{
vot3Token: await vot3.getAddress(),
timelock: await timeLock.getAddress(),
xAllocationVoting: await xAllocationVotingV2.getAddress(),
b3tr: await b3tr.getAddress(),
quorumPercentage: config.B3TR_GOVERNOR_QUORUM_PERCENTAGE, // quorum percentage
initialDepositThreshold: config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD, // deposit threshold
initialMinVotingDelay: config.B3TR_GOVERNOR_MIN_VOTING_DELAY, // delay before vote starts
initialVotingThreshold: config.B3TR_GOVERNOR_VOTING_THRESHOLD, // voting threshold
voterRewards: await voterRewardsV1.getAddress(),
isFunctionRestrictionEnabled: true,
},
{
governorAdmin: owner.address,
pauser: owner.address,
contractsAddressManager: owner.address,
proposalExecutor: owner.address,
governorFunctionSettingsRoleAddress: owner.address,
},
],
[],
[],
[await veBetterPassport.getAddress()],
], {
versions: [undefined, 2, 3, 4],
libraries: [
{
GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(),
GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(),
GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(),
GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(),
GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(),
GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(),
GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(),
GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(),
},
{
GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(),
GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(),
GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(),
GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(),
GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(),
GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(),
GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(),
GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(),
},
{
GovernorClockLogicV3: await governorClockLogicLibV3.getAddress(),
GovernorConfiguratorV3: await governorConfiguratorLibV3.getAddress(),
GovernorDepositLogicV3: await governorDepositLogicLibV3.getAddress(),
GovernorFunctionRestrictionsLogicV3: await governorFunctionRestrictionsLogicLibV3.getAddress(),
GovernorProposalLogicV3: await governorProposalLogicLibV3.getAddress(),
GovernorQuorumLogicV3: await governorQuorumLogicLibV3.getAddress(),
GovernorStateLogicV3: await governorStateLogicLibV3.getAddress(),
GovernorVotesLogicV3: await governorVotesLogicLibV3.getAddress(),
},
{
GovernorClockLogicV4: await governorClockLogicLibV4.getAddress(),
GovernorConfiguratorV4: await governorConfiguratorLibV4.getAddress(),
GovernorDepositLogicV4: await governorDepositLogicLibV4.getAddress(),
GovernorFunctionRestrictionsLogicV4: await governorFunctionRestrictionsLogicLibV4.getAddress(),
GovernorProposalLogicV4: await governorProposalLogicLibV4.getAddress(),
GovernorQuorumLogicV4: await governorQuorumLogicLibV4.getAddress(),
GovernorStateLogicV4: await governorStateLogicLibV4.getAddress(),
GovernorVotesLogicV4: await governorVotesLogicLibV4.getAddress(),
},
],
}));
await xAllocationPool.connect(owner).setXAllocationVotingAddress(await xAllocationVotingV2.getAddress());
// Grant Vote registrar role to XAllocationVoting
await voterRewardsV1
.connect(owner)
.grantRole(await voterRewardsV1.VOTE_REGISTRAR_ROLE(), await xAllocationVotingV2.getAddress());
// Grant Vote registrar role to Governor
await voterRewardsV1
.connect(owner)
.grantRole(await voterRewardsV1.VOTE_REGISTRAR_ROLE(), await governorV4.getAddress());
// Grant admin role to voter rewards for registering x allocation voting
await xAllocationVotingV2
.connect(owner)
.grantRole(await xAllocationVotingV2.DEFAULT_ADMIN_ROLE(), emissions.getAddress());
// Set xAllocationGovernor in emissions
await emissions.connect(owner).setXAllocationsGovernorAddress(await xAllocationVotingV2.getAddress());
await emissions.connect(owner).setVote2EarnAddress(await voterRewardsV1.getAddress());
// Setup XAllocationPool addresses
await xAllocationPool.connect(owner).setXAllocationVotingAddress(await xAllocationVotingV2.getAddress());
await xAllocationPool.connect(owner).setEmissionsAddress(await emissions.getAddress());
//Set the emissions address and the admin as the ROUND_STARTER_ROLE in XAllocationVoting
const roundStarterRole = await xAllocationVotingV2.ROUND_STARTER_ROLE();
await xAllocationVotingV2
.connect(owner)
.grantRole(roundStarterRole, await emissions.getAddress())
.then(async (tx) => await tx.wait());
await xAllocationVotingV2
.connect(owner)
.grantRole(roundStarterRole, owner.address)
.then(async (tx) => await tx.wait());
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 getVot3Tokens(voter1, "1000");
await getVot3Tokens(voter2, "1000");
await getVot3Tokens(voter3, "1000");
// Bootstrap emissions
await bootstrapAndStartEmissions();
const roundId = await xAllocationVotingV2.currentRoundId();
expect(roundId).to.equal(1);
expect(Number(await xAllocationVotingV2.roundDeadline(roundId))).to.lt(Number(await emissions.getNextCycleBlock()));
await veBetterPassport.whitelist(voter1.address);
await veBetterPassport.whitelist(voter2.address);
await veBetterPassport.whitelist(voter3.address);
await veBetterPassport.toggleCheck(1);
await xAllocationVotingV2.connect(voter1).castVote(roundId, [app1], [ethers.parseEther("1000")]);
await xAllocationVotingV2
.connect(voter2)
.castVote(roundId, [app1, app2], [ethers.parseEther("200"), ethers.parseEther("100")]);
await xAllocationVotingV2
.connect(voter3)
.castVote(roundId, [app1, app2], [ethers.parseEther("500"), ethers.parseEther("500")]);
expect(await emissions.isCycleEnded(1)).to.equal(false);
await catchRevert(voterRewardsV1.claimReward(1, voter1.address));
expect(await voterRewardsV1.cycleToVoterToTotal(1, voter1)).to.equal(ethers.parseEther("31.622776601"));
expect(await voterRewardsV1.cycleToVoterToTotal(1, voter2)).to.equal(ethers.parseEther("17.320508075"));
await catchRevert(voterRewardsV1.claimReward(1, voter2.address));
expect(await voterRewardsV1.cycleToVoterToTotal(1, voter3)).to.equal(ethers.parseEther("31.622776601"));
// Votes should be tracked correctly
let appVotes = await xAllocationVotingV2.getAppVotes(roundId, app1);
expect(appVotes).to.eql(ethers.parseEther("1700"));
appVotes = await xAllocationVotingV2.getAppVotes(roundId, app2);
expect(appVotes).to.eql(ethers.parseEther("600"));
let totalVotes = await xAllocationVotingV2.totalVotes(roundId);
expect(totalVotes).to.eql(ethers.parseEther("2300"));
// Total voters should be tracked correctly
let totalVoters = await xAllocationVotingV2.totalVoters(roundId);
expect(totalVoters).to.eql(BigInt(3));
// Voter rewards checks
expect(await voterRewardsV1.cycleToTotal(1)).to.equal(ethers.parseEther("80.566061277")); // Total votes -> Math.sqrt(1000) + Math.sqrt(300) + Math.sqrt(1000)
expect(await voterRewardsV1.cycleToTotal(1)).to.equal((await voterRewardsV1.cycleToVoterToTotal(1, voter1)) +
(await voterRewardsV1.cycleToVoterToTotal(1, voter2)) +
(await voterRewardsV1.cycleToVoterToTotal(1, voter3))); // Total votes
let storageSlots = [];
const initialSlot = BigInt("0x114e7ffaaf205d38cd05b17b56f3357806ef2ce889cb4748445ae91cdfc37c00"); // Slot 0 of VoterRewards
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlots.push(await ethers.provider.getStorage(await voterRewardsV1.getAddress(), i));
}
storageSlots = storageSlots.filter(slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000"); // removing empty slots
const voterRewardsV2 = (await upgradeProxy("VoterRewardsV1", "VoterRewardsV2", await voterRewardsV1.getAddress(), [], {
version: 2,
}));
let storageSlotsAfter = [];
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlotsAfter.push(await ethers.provider.getStorage(await voterRewardsV2.getAddress(), i));
}
// Check if storage slots are the same after upgrade
for (let i = 0; i < storageSlots.length; i++) {
expect(storageSlots[i]).to.equal(storageSlotsAfter[i]);
}
await waitForRoundToEnd(Number(roundId));
// Votes should be the same after round ended
appVotes = await xAllocationVotingV2.getAppVotes(roundId, app1);
expect(appVotes).to.eql(ethers.parseEther("1700"));
appVotes = await xAllocationVotingV2.getAppVotes(roundId, app2);
expect(appVotes).to.eql(ethers.parseEther("600"));
totalVotes = await xAllocationVotingV2.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 voterRewardsV2.getAddress())).to.equal(await emissions.getVote2EarnAmount(1));
const voter1Rewards = await voterRewardsV2.getReward(1, voter1.address);
const voter2Rewards = await voterRewardsV2.getReward(1, voter2.address);
const voter3Rewards = await voterRewardsV2.getReward(1, voter3.address);
await voterRewardsV2.connect(voter1).claimReward(1, voter1);
expect(await b3tr.balanceOf(voter1.address)).to.equal(voter1Rewards);
expect(await b3tr.balanceOf(await voterRewardsV2.getAddress())).to.equal((await emissions.getVote2EarnAmount(1)) - voter1Rewards);
// Second round
await emissions.connect(voter1).distribute(); // Anyone can distribute the cycle
const roundId2 = await xAllocationVotingV2.currentRoundId();
expect(roundId2).to.equal(2);
expect(Number(await xAllocationVotingV2.roundDeadline(roundId))).to.lt(Number(await emissions.getNextCycleBlock()));
await xAllocationVotingV2.connect(voter1).castVote(roundId2, [app2], [ethers.parseEther("1000")]);
await xAllocationVotingV2
.connect(voter2)
.castVote(roundId2, [app1, app2], [ethers.parseEther("100"), ethers.parseEther("500")]);
await xAllocationVotingV2
.connect(voter3)
.castVote(roundId2, [app1, app2], [ethers.parseEther("500"), ethers.parseEther("500")]);
expect(await emissions.isCycleEnded(2)).to.equal(false);
await catchRevert(voterRewardsV2.claimReward(2, voter1.address));
expect(await voterRewardsV2.cycleToVoterToTotal(2, voter1)).to.equal(ethers.parseEther("31.622776601"));
expect(await voterRewardsV2.cycleToVoterToTotal(2, voter2)).to.equal(ethers.parseEther("24.494897427"));
await catchRevert(voterRewardsV2.claimReward(2, voter2.address));
expect(await voterRewardsV2.cycleToVoterToTotal(2, voter3)).to.equal(ethers.parseEther("31.622776601"));
// Votes should be tracked correctly
appVotes = await xAllocationVotingV2.getAppVotes(roundId2, app1);
expect(appVotes).to.eql(ethers.parseEther("600"));
appVotes = await xAllocationVotingV2.getAppVotes(roundId2, app2);
expect(appVotes).to.eql(ethers.parseEther("2000"));
totalVotes = await xAllocationVotingV2.totalVotes(roundId2);
expect(totalVotes).to.eql(ethers.parseEther("2600"));
// Total voters should be tracked correctly
totalVoters = await xAllocationVotingV2.totalVoters(roundId2);
expect(totalVoters).to.eql(BigInt(3));
// Voter rewards checks
expect(await voterRewardsV2.cycleToTotal(2)).to.equal(ethers.parseEther("87.740450629")); // Total votes -> Math.sqrt(1000) + Math.sqrt(300) + Math.sqrt(1000)
expect(await voterRewardsV2.cycleToTotal(2)).to.equal((await voterRewardsV2.cycleToVoterToTotal(2, voter1)) +
(await voterRewardsV2.cycleToVoterToTotal(2, voter2)) +
(await voterRewardsV2.cycleToVoterToTotal(2, voter3))); // Total votes
await waitForRoundToEnd(Number(roundId2));
// Votes should be the same after round ended
appVotes = await xAllocationVotingV2.getAppVotes(roundId2, app1);
expect(appVotes).to.eql(ethers.parseEther("600"));
appVotes = await xAllocationVotingV2.getAppVotes(roundId2, app2);
expect(appVotes).to.eql(ethers.parseEther("2000"));
totalVotes = await xAllocationVotingV2.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(Number(await b3tr.balanceOf(await voterRewardsV2.getAddress()))).to.gt(Number(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 voterRewardsV2.getReward(2, voter1.address);
const voter2Rewards2 = await voterRewardsV2.getReward(2, voter2.address);
const voter3Rewards2 = await voterRewardsV2.getReward(2, voter3.address);
await voterRewardsV2.connect(voter1).claimReward(2, voter1);
await voterRewardsV2.connect(voter2).claimReward(2, voter2);
await voterRewardsV2.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 voterRewardsV2.connect(voter2).claimReward(1, voter2);
await voterRewardsV2.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);
// Check if storage slots are the same after upgrade
storageSlots = [];
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlots.push(await ethers.provider.getStorage(await voterRewardsV2.getAddress(), i));
}
;
(await upgradeProxy("GalaxyMemberV1", "GalaxyMemberV2", await galaxyMemberV1.getAddress(), [
await vechainNodesMock.getAddress(),
await nodeManagement.getAddress(),
owner.address,
config.GM_NFT_NODE_TO_FREE_LEVEL,
], { version: 2 }));
const voterRewardsV3 = (await upgradeProxy("VoterRewardsV2", "VoterRewardsV3", await voterRewardsV1.getAddress(), [], {
version: 3,
}));
await waitForNextCycle();
// start round
await emissions.connect(voter1).distribute(); // Anyone can distribute the cycle
const roundId3 = await xAllocationVotingV2.currentRoundId();
expect(roundId3).to.equal(3);
await xAllocationVotingV2
.connect(voter1)
.castVote(roundId3, [app1, app2], [ethers.parseEther("0"), ethers.parseEther("1000")]);
await xAllocationVotingV2
.connect(voter2)
.castVote(roundId3, [app1, app2], [ethers.parseEther("100"), ethers.parseEther("500")]);
await waitForRoundToEnd(Number(roundId3));
// Check storage slots after upgrade
storageSlotsAfter = [];
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlotsAfter.push(await ethers.provider.getStorage(await voterRewardsV3.getAddress(), i));
}
// Check if storage slots are the same after upgrade
for (let i = 0; i < storageSlots.length; i++) {
expect(storageSlots[i]).to.equal(storageSlotsAfter[i]);
}
// Upgrade to V3
await upgradeProxy("GalaxyMemberV2", "GalaxyMemberV3", await galaxyMemberV1.getAddress(), [], {
version: 3,
});
const voterRewardsV4 = (await upgradeProxy("VoterRewardsV3", "VoterRewardsV4", await voterRewardsV1.getAddress(), [], {
version: 3,
}));
let storageSlotsV4 = [];
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlotsV4.push(await ethers.provider.getStorage(await voterRewardsV4.getAddress(), i));
}
// Check if storage slots are the same after upgrade
for (let i = 0; i < storageSlots.length; i++) {
expect(storageSlotsAfter[i]).to.equal(storageSlotsV4[i]);
}
await waitForNextCycle();
// start round
await emissions.connect(voter1).distribute(); // Anyone can distribute the cycle
const roundId4 = await xAllocationVotingV2.currentRoundId();
expect(roundId4).to.equal(4);
await xAllocationVotingV2
.connect(voter1)
.castVote(roundId4, [app1, app2], [ethers.parseEther("0"), ethers.parseEther("1000")]);
await xAllocationVotingV2
.connect(voter2)
.castVote(roundId4, [app1, app2], [ethers.parseEther("100"), ethers.parseEther("500")]);
// Wait for round to end
const deadline = await xAllocationVotingV2.roundDeadline(roundId4);
const currentBlock = await xAllocationVotingV2.clock();
await moveBlocks(parseInt((deadline - currentBlock + BigInt(1)).toString()));
await expect(voterRewardsV4.connect(voter1).claimReward(4, voter1)).to.emit(voterRewardsV4, "RewardClaimed");
await expect(voterRewardsV4.connect(voter2).claimReward(4, voter2)).to.emit(voterRewardsV4, "RewardClaimed");
});
it("Should not have state conflict after upgrading to V5 and then to V6", async () => {
// ========================================
// INITIAL SETUP AND CONTRACT DEPLOYMENT
// ========================================
const config = createLocalConfig();
const contractConfig = await getOrDeployContractInstances({
forceDeploy: true,
});
if (!contractConfig)
throw new Error("Failed to deploy contracts");
const { otherAccounts, otherAccount: voter1, owner, b3tr, timeLock, nodeManagement, vot3, treasury, x2EarnApps, veBetterPassport, xAllocationPool, governorClockLogicLibV1, governorConfiguratorLibV1, governorDepositLogicLibV1, governorFunctionRestrictionsLogicLibV1, governorProposalLogicLibV1, governorQuorumLogicLibV1, governorStateLogicLibV1, governorVotesLogicLibV1, governorClockLogicLibV3, governorConfiguratorLibV3, governorDepositLogicLibV3, governorFunctionRestrictionsLogicLibV3, governorProposalLogicLibV3, governorQuorumLogicLibV3, governorStateLogicLibV3, governorVotesLogicLibV3, governorClockLogicLibV4, governorConfiguratorLibV4, governorDepositLogicLibV4, governorFunctionRestrictionsLogicLibV4, governorProposalLogicLibV4, governorQuorumLogicLibV4, governorStateLogicLibV4, governorVotesLogicLibV4, governorClockLogicLibV5, governorConfiguratorLibV5, governorDepositLogicLibV5, governorFunctionRestrictionsLogicLibV5, governorQuorumLogicLibV5, governorStateLogicLibV5, governorVotesLogicLibV5, governorProposalLogicLibV5, governorClockLogicLibV6, governorConfiguratorLibV6, governorDepositLogicLibV6, governorFunctionRestrictionsLogicLibV6, governorProposalLogicLibV6, governorQuorumLogicLibV6, governorStateLogicLibV6, governorVotesLogicLibV6, governorClockLogicLib, governorConfiguratorLib, governorDepositLogicLib, governorFunctionRestrictionsLogicLib, governorProposalLogicLib, governorQuorumLogicLib, governorStateLogicLib, governorVotesLogicLib, minterAccount, } = contractConfig;
const galaxyMember = (await deployAndUpgrade(["GalaxyMemberV1", "GalaxyMemberV2"], [
[
{
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(),
},
],
[owner.address, await nodeManagement.getAddress(), owner.address, config.GM_NFT_NODE_TO_FREE_LEVEL],
], { versions: [undefined, 2] }));
// Deploy Emissions contract
const emissions = (await deployAndUpgrade(["EmissionsV1", "EmissionsV2"], [
[
{
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,
},
],
[config.EMISSIONS_IS_NOT_ALIGNED],
], {
versions: [undefined, 2],
logOutput: false,
}));
// Deploy VoterRewards V4 contract
const voterRewardsV4 = (await deployAndUpgrade(["VoterRewardsV1", "VoterRewardsV2", "VoterRewardsV3", "VoterRewardsV4"], [
[
owner.address, // admin
owner.address, // upgrader
owner.address, // contractsAddressManager
await emissions.getAddress(),
await galaxyMember.getAddress(),
await b3tr.getAddress(),
levels,
multipliers,
],
[],
[],
[],
], {
versions: [undefined, 2, 3, 4],
}));
await voterRewardsV4.connect(owner).setEmissions(await emissions.getAddress());
// Deploy XAllocationVoting contract
const xAllocationVoting = (await deployAndUpgrade([
"XAllocationVotingV1",
"XAllocationVotingV2",
"XAllocationVotingV3",
"XAllocationVotingV4",
"XAllocationVotingV5",
], [
[
{
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 voterRewardsV4.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, 4, 5],
logOutput: false,
}));
// Deploy B3TR Governor V5 contract
const governor = (await deployAndUpgrade(["B3TRGovernorV1", "B3TRGovernorV2", "B3TRGovernorV3", "B3TRGovernorV4", "B3TRGovernorV5"], [
[
{
vot3Token: await vot3.getAddress(),
timelock: await timeLock.getAddress(),
xAllocationVoting: await xAllocationVoting.getAddress(),
b3tr: await b3tr.getAddress(),
quorumPercentage: config.B3TR_GOVERNOR_QUORUM_PERCENTAGE,
initialDepositThreshold: config.B3TR_GOVERNOR_DEPOSIT_THRESHOLD,
initialMinVotingDelay: config.B3TR_GOVERNOR_MIN_VOTING_DELAY,
initialVotingThreshold: config.B3TR_GOVERNOR_VOTING_THRESHOLD,
voterRewards: await voterRewardsV4.getAddress(),
isFunctionRestrictionEnabled: true,
},
{
governorAdmin: owner.address, // admin
pauser: owner.address, // botSignaler
contractsAddressManager: owner.address, // upgrader
proposalExecutor: owner.address, // settingsManager
governorFunctionSettingsRoleAddress: owner.address, // roleGranter
},
],
[],
[],
[await veBetterPassport.getAddress()],
[],
], {
versions: [undefined, 2, 3, 4, 5],
libraries: [
{
GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(),
GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(),
GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(),
GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(),
GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(),
GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(),
GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(),
GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(),
},
{
GovernorClockLogicV1: await governorClockLogicLibV1.getAddress(),
GovernorConfiguratorV1: await governorConfiguratorLibV1.getAddress(),
GovernorDepositLogicV1: await governorDepositLogicLibV1.getAddress(),
GovernorFunctionRestrictionsLogicV1: await governorFunctionRestrictionsLogicLibV1.getAddress(),
GovernorProposalLogicV1: await governorProposalLogicLibV1.getAddress(),
GovernorQuorumLogicV1: await governorQuorumLogicLibV1.getAddress(),
GovernorStateLogicV1: await governorStateLogicLibV1.getAddress(),
GovernorVotesLogicV1: await governorVotesLogicLibV1.getAddress(),
},
{
GovernorClockLogicV3: await governorClockLogicLibV3.getAddress(),
GovernorConfiguratorV3: await governorConfiguratorLibV3.getAddress(),
GovernorDepositLogicV3: await governorDepositLogicLibV3.getAddress(),
GovernorFunctionRestrictionsLogicV3: await governorFunctionRestrictionsLogicLibV3.getAddress(),
GovernorProposalLogicV3: await governorProposalLogicLibV3.getAddress(),
GovernorQuorumLogicV3: await governorQuorumLogicLibV3.getAddress(),
GovernorStateLogicV3: await governorStateLogicLibV3.getAddress(),
GovernorVotesLogicV3: await governorVotesLogicLibV3.getAddress(),
},
{
GovernorClockLogicV4: await governorClockLogicLibV4.getAddress(),
GovernorConfiguratorV4: await governorConfiguratorLibV4.getAddress(),
GovernorDepositLogicV4: await governorDepositLogicLibV4.getAddress(),
GovernorFunctionRestrictionsLogicV4: await governorFunctionRestrictionsLogicLibV4.getAddress(),
GovernorProposalLogicV4: await governorProposalLogicLibV4.getAddress(),
GovernorQuorumLogicV4: await governorQuorumLogicLibV4.getAddress(),
GovernorStateLogicV4: await governorStateLogicLibV4.getAddress(),
GovernorVotesLogicV4: await governorVotesLogicLibV4.getAddress(),
},
{
GovernorClockLogicV5: await governorClockLogicLibV5.getAddress(),
GovernorConfiguratorV5: await governorConfiguratorLibV5.getAddress(),
GovernorDepositLogicV5: await governorDepositLogicLibV5.getAddress(),
GovernorFunctionRestrictionsLogicV5: await governorFunctionRestrictionsLogicLibV5.getAddress(),
GovernorProposalLogicV5: await governorProposalLogicLibV5.getAddress(),
GovernorQuorumLogicV5: await governorQuorumLogicLibV5.getAddress(),
GovernorStateLogicV5: await governorStateLogicLibV5.getAddress(),
GovernorVotesLogicV5: await governorVotesLogicLibV5.getAddress(),
},
],
}));
// ========================================
// SETUP ROLES AND PERMISSIONS
// ========================================
await xAllocationPool.connect(owner).setXAllocationVotingAddress(await xAllocationVoting.getAddress());
// Grant Vote registrar role to XAllocationVoting
await voterRewardsV4
.connect(owner)
.grantRole(await voterRewardsV4.VOTE_REGISTRAR_ROLE(), await xAllocationVoting.getAddress());
// Grant Vote registrar role to Governor
await voterRewardsV4
.connect(owner)
.grantRole(await voterRewardsV4.VOTE_REGISTRAR_ROLE(), await governor.getAddress());
// Grant admin role to voter rewards for registering x allocation voting
await xAllocationVoting
.connect(owner)
.grantRole(await xAllocationVoting.DEFAULT_ADMIN_ROLE(), emissions.getAddress());
// Set xAllocationGovernor in emissions
await emissions.connect(owner).setXAllocationsGovernorAddress(await xAllocationVoting.getAddress());
await emissions.connect(owner).setVote2EarnAddress(await voterRewardsV4.getAddress());
// Setup XAllocationPool addresses
await xAllocationPool.connect(owner).setXAllocationVotingAddress(await xAllocationVoting.getAddress());
await xAllocationPool.connect(owner).setEmissionsAddress(await emissions.getAddress());
await b3tr.connect(owner).grantRole(await b3tr.MINTER_ROLE(), await emissions.getAddress());
//Set the emissions address and the admin as the ROUND_STARTER_ROLE in XAllocationVoting
const roundStarterRole = await xAllocationVoting.ROUND_STARTER_ROLE();
await xAllocationVoting
.connect(owner)
.grantRole(roundStarterRole, await emissions.getAddress())
.then(async (tx) => await tx.wait());
await xAllocationVoting
.connect(owner)
.grantRole(roundStarterRole, owner.address)
.then(async (tx) => await tx.wait());
await galaxyMember.connect(owner).setB3trGovernorAddress(await governor.getAddress());
await galaxyMember.connect(owner).setXAllocationsGovernorAddress(await xAllocationVoting.getAddress());
// ========================================
// SETUP APPS AND VOTERS
// ========================================
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 getVot3Tokens(voter1, "1000");
await getVot3Tokens(voter2, "1000");
await getVot3Tokens(voter3, "1000");
await veBetterPassport.whitelist(voter1.address);
await veBetterPassport.whitelist(voter2.address);
await veBetterPassport.whitelist(voter3.address);
await veBetterPassport.toggleCheck(1);
// ========================================
// START EMISSIONS AND FIRST ROUND
// ========================================
// Bootstrap emissions
await emissions.connect(minterAccount).bootstrap();
await emissions.connect(minterAccount).start();
const roundId = await xAllocationVoting.currentRoundId();
expect(roundId).to.equal(1);
expect(await xAllocationVoting.roundDeadline(roundId)).to.lt(await emissions.getNextCycleBlock());
// ========================================
// ROUND 1: VOTING (PRE-UPGRADE)
// ========================================
// 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("500"), ethers.parseEther("500")], // Voter 2 votes 500 for app1 and 500 for app2
[ethers.parseEther("500"), ethers.parseEther("500")], // Voter 3 votes 500 for app1 and 500 for app2
], roundId, // First round
xAllocationVoting, veBetterPassport);
// Track Round 1 rewards before claiming
const round1Voter1Reward = await voterRewardsV4.getReward(1, voter1.address);
const round1Voter2Reward = await voterRewardsV4.getReward(1, voter2.address);
const round1Voter3Reward = await voterRewardsV4.getReward(1, voter3.address);
// Rewards to be claimed are the same for all voters:
expect(await voterRewardsV4.getReward(1, voter1.address)).to.equal(666666666666666666666666n);
expect(await voterRewardsV4.getReward(1, voter2.address)).to.equal(666666666666666666666666n);
expect(await voterRewardsV4.getReward(1, voter3.address)).to.equal(666666666666666666666666n);
await waitForRoundToEnd(Number(roundId), xAllocationVoting);
await waitForNextCycle(emissions);
// Claim Round 1 rewards - verify basic functionality works
await expect(voterRewardsV4.connect(voter1).claimReward(1, voter1.address))
.to.emit(voterRewardsV4, "RewardClaimed")
.withArgs(1, voter1.address, 666666666666666666666666n);
await expect(voterRewardsV4.connect(voter2).claimReward(1, voter2.address))
.to.emit(voterRewardsV4, "RewardClaimed")
.withArgs(1, voter2.address, 666666666666666666666666n);
await expect(voterRewardsV4.connect(voter3).claimReward(1, voter3.address))
.to.emit(voterRewardsV4, "RewardClaimed")
.withArgs(1, voter3.address, 666666666666666666666666n);
// ========================================
// SETUP GM NFTS FOR MULTIPLIERS
// ========================================
// GM NFT token mint and upgrade
await galaxyMember.connect(voter1).freeMint();
await upgradeNFTtoLevel(1, 5, galaxyMember, b3tr, voter1, minterAccount); // Upgrading to level 5
expect(await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(voter1.address))).to.equal(5);
await galaxyMember.connect(voter2).freeMint();
await upgradeNFTtoLevel(2, 10, galaxyMember, b3tr, voter2, minterAccount); // Upgrading to level 10
expect(await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(voter2.address))).to.equal(10);
await galaxyMember.connect(voter3).freeMint();
await upgradeNFTtoLevel(3, 2, galaxyMember, b3tr, voter3, minterAccount); // Upgrading to level 2
expect(await galaxyMember.levelOf(await galaxyMember.getSelectedTokenId(voter3.address))).to.equal(2);
// ========================================
// ROUND 2: VOTING (BEFORE V5 UPGRADE)
// ========================================
await emissions.connect(voter1).distribute(); // Anyone can distribute the cycle
const roundId2 = await xAllocationVoting.currentRoundId();
expect(roundId2).to.equal(2);
expect(await xAllocationVoting.roundDeadline(roundId2)).to.lt(await emissions.getNextCycleBlock());
await waitForNextBlock();
// ========================================
// STORAGE VERIFICATION BEFORE V5 UPGRADE
// ========================================
let storageSlots = [];
const initialSlot = BigInt("0x114e7ffaaf205d38cd05b17b56f3357806ef2ce889cb4748445ae91cdfc37c00"); // Slot 0 of VoterRewards
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlots.push(await ethers.provider.getStorage(await voterRewardsV4.getAddress(), i));
}
storageSlots = storageSlots.filter(slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000"); // removing empty slots
// ========================================
// UPGRADE TO V5 (GM POOL SUPPORT)
// ========================================
// Upgrade Emissions and Voter Rewards to support GM Pool
const emissionsLatest = (await upgradeProxy("EmissionsV2", "Emissions", await emissions.getAddress(), [config.GM_PERCENTAGE_OF_TREASURY], {
version: 3,
}));
const voterRewardsV5 = (await upgradeProxy("VoterRewardsV4", "VoterRewardsV5", await voterRewardsV4.getAddress(), [config.VOTER_REWARDS_LEVELS_V2, config.GM_MULTIPLIERS_V2], {
version: 5,
}));
// Upgrade contracts that use interfaces
await upgradeProxy("XAllocationVotingV5", "XAllocationVotingV6", await xAllocationVoting.getAddress(), [], {
version: 6,
});
await upgradeProxy("XAllocationPoolV5", "XAllocationPool", await xAllocationPool.getAddress(), [], {
version: 6,
});
await upgradeProxy("B3TRGovernorV5", "B3TRGovernorV6", await governor.getAddress(), [], {
version: 6,
libraries: {
GovernorClockLogicV6: await governorClockLogicLibV6.getAddress(),
GovernorConfiguratorV6: await governorConfiguratorLibV6.getAddress(),
GovernorDepositLogicV6: await governorDepositLogicLibV6.getAddress(),
GovernorFunctionRestrictionsLogicV6: await governorFunctionRestrictionsLogicLibV6.getAddress(),
GovernorProposalLogicV6: await governorProposalLogicLibV6.getAddress(),
GovernorQuorumLogicV6: await governorQuorumLogicLibV6.getAddress(),
GovernorStateLogicV6: await governorStateLogicLibV6.getAddress(),
GovernorVotesLogicV6: await governorVotesLogicLibV6.getAddress(),
},
});
let storageSlotsAfter = [];
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlotsAfter.push(await ethers.provider.getStorage(await voterRewardsV5.getAddress(), i));
}
// Check if storage slots are the same after upgrade
for (let i = 0; i < storageSlots.length; i++) {
expect(storageSlots[i]).to.equal(storageSlotsAfter[i]);
}
expect(await voterRewardsV5.version()).to.equal("5");
// ========================================
// ROUND 2: VOTING (AFTER V5 UPGRADE)
// ========================================
// 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("500"), ethers.parseEther("500")], // Voter 2 votes 500 for app1 and 500 for app2
[ethers.parseEther("500"), ethers.parseEther("500")], // Voter 3 votes 500 for app1 and 500 for app2
], roundId2, // Second round
xAllocationVoting, veBetterPassport);
// Track Round 2 rewards before claiming
const round2Voter1Reward = await voterRewardsV5.getReward(2, voter1.address);
const round2Voter2Reward = await voterRewardsV5.getReward(2, voter2.address);
const round2Voter3Reward = await voterRewardsV5.getReward(2, voter3.address);
const round2Voter1GMReward = await voterRewardsV5.getGMReward(2, voter1.address);
const round2Voter2GMReward = await voterRewardsV5.getGMReward(2, voter2.address);
const round2Voter3GMReward = await voterRewardsV5.getGMReward(2, voter3.address);
//------ Should still be using original GM reward formula for the round when upgrade happens ------//
/*
voter 1 = 31.62 reward weighted votes * 100% multiplier = 63.24 reward weighted power
voter 2 = 31.62 reward weighted votes * 25x multiplier = 790.57 reward weighted power
voter 3 = 31.62 reward weighted votes * 10% multiplier = 34.78 reward weighted power
Total power = 888.60
voter 1 allocation = 63.24 / 888.60 * 100 = 7.11%
voter 2 allocation = 790.57 / 888.60 * 100 = 88.96%
voter 3 allocation = 34.78 / 888.60 * 100 = 3.91%
*/
expect(await voterRewardsV5.cycleToTotal(2)).to.equal(ethers.parseEther("888.6000224881")); // Total reward weighted votes
expect(await voterRewardsV5.cycleToVoterToTotal(2, voter1)).to.equal(ethers.parseEther("63.245553202")); // Voter 1 reward weighted votes
expect(await voterRewardsV5.cycleToVoterToTotal(2, voter2)).to.equal(ethers.parseEther("790.569415025")); // Voter 2 reward weighted votes
expect(await voterRewardsV5.cycleToVoterToTotal(2, voter3)).to.equal(ethers.parseEther("34.7850542611")); // Voter 3 reward weighted votes
/*
voter 1 = 31.62 reward weighted votes * 100% multiplier = 63.24 reward weighted power
voter 2 = 31.62 reward weighted votes * 25x multiplier = 790.57 reward weighted power
voter 3 = 31.62 reward weighted votes * 10% multiplier = 34.78 reward weighted power
Total power = 888.60
voter 1 allocation = 63.24 / 888.60 * 100 = 7.11%
voter 2 allocation = 790.57 / 888.60 * 100 = 88.96%
voter 3 allocation = 34.78 / 888.60 * 100 = 3.91%
*/
expect(await voterRewardsV5.getReward(2, voter1.address)).to.equal(142348754448398576512455n); // 7.11%
expect(await voterRewardsV5.getReward(2, voter2.address)).to.equal(1779359430604982206405693n); // 88.96%
expect(await voterRewardsV5.getReward(2, voter3.address)).to.equal(78291814946619217081850n); // 3.91%
// Get GM rewards should be 0
expect(await voterRewardsV5.getGMReward(2, voter1.address)).to.equal(0n);
expect(await voterRewardsV5.getGMReward(2, voter2.address)).to.equal(0n);
expect(await voterRewardsV5.getGMReward(2, voter3.address)).to.equal(0n);
// GM pool is 0 for round 2
expect(await emissionsLatest.getGMAmount(2)).to.equal(0n);
// wait for next cycle
await waitForNextCycle(emissionsLatest);
// Distribute emissions
await emissionsLatest.connect(minterAccount).distribute();
// GM pool should have 25% of the treasury
expect(await emissionsLatest.getGMAmount(3)).to.equal(ethers.parseEther("250000"));
// Claim Round 2 rewards - verify V5 functionality works
await expect(voterRewardsV5.connect(voter1).claimReward(2, voter1.address))
.to.emit(voterRewardsV5, "RewardClaimedV2")
.withArgs(2, voter1.address, 142348754448398576512455n, 0n);
await expect(voterRewardsV5.connect(voter2).claimReward(2, voter2.address))
.to.emit(voterRewardsV5, "RewardClaimedV2")
.withArgs(2, voter2.address, 1779359430604982206405693n, 0n);
await expect(voterRewardsV5.connect(voter3).claimReward(2, voter3.address))
.to.emit(voterRewardsV5, "RewardClaimedV2")
.withArgs(2, voter3.address, 78291814946619217081850n, 0n);
// ========================================
// ROUND 3: VOTING (V5 WITH GM POOL)
// ========================================
const roundId3 = await xAllocationVoting.currentRoundId();
// Vote on apps for the third round
await voteOnApps([app1, app2], [voter1, voter2, voter3], [
[ethers.parseEther("0"), ethers.parseEther("1000")], // Voter 1 votes 1000 for app2
[ethers.parseEther("500"), ethers.parseEther("500")], // Voter 2 votes 500 for app1 and 500 for app2
[ethers.parseEther("500"), ethers.parseEther("500")], // Voter 3 votes 500 for app1 and 500 for app2
], roundId3, // Third round
xAllocationVoting, veBetterPassport);
// Track Round 3 rewards before claiming
const round3Voter1Reward = await voterRewardsV5.getReward(3, voter1.address);
const round3Voter2Reward = await voterRewardsV5.getReward(3, voter2.address);
const round3Voter3Reward = await voterRewardsV5.getReward(3, voter3.address);
const round3Voter1GMReward = await voterRewardsV5.getGMReward(3, voter1.address);
const round3Voter2GMReward = await voterRewardsV5.getGMReward(3, voter2.address);
const round3Voter3GMReward = await voterRewardsV5.getGMReward(3, voter3.address);
// Total votes will no longer be effected by GM as that comes from standalone pool
expect(await voterRewardsV5.cycleToTotal(3)).to.equal(ethers.parseEther("94.868329803")); // Total weighted votes (31.62 * 3)
expect(await voterRewardsV5.cycleToVoterToTotal(3, voter1)).to.equal(ethers.parseEther("31.622776601")); // Voter 1 weighted vote
expect(await voterRewardsV5.cycleToVoterToTotal(3, voter2)).to.equal(ethers.parseEther("31.622776601")); // Voter 2 weighted vote
expect(await voterRewardsV5.cycleToVoterToTotal(3, voter3)).to.equal(ethers.parseEther("31.622776601")); // Voter 3 weighted vote
// Total GM weight should be 2810
/**
* voter 1 has level 5 GM NFT -> 2x multiplier (200)
* voter 2 has level 10 GM NFT -> 25x multiplier (2500)
* voter 3 has level 2 GM NFT -> 1.1x multiplier (110)
*
* total = 200 + 2500 + 110 = 2810
*/
expect(await voterRewardsV5.cycleToTotalGMWeight(3)).to.equal(2810n);
expect(await voterRewardsV5.cycleToVoterToGMWeight(3, voter1)).to.equal(200n);
expect(await voterRewardsV5.cycleToVoterToGMWeight(3, voter2)).to.equal(2500n);
expect(await voterRewardsV5.cycleToVoterToGMWeight(3, voter3)).to.equal(110n);
/**
* There is 250000 in the GM pool for round 3
*
* Rewards distribution should be:
* voter 1 = 200 / 2810 * 250000 = 17793.59 B3TR
* voter 2 = 2500 / 2810 * 250000 = 222419.92 B3TR
* voter 3 = 110 / 2810 * 250000 = 9786.45 B3TR
*/
expect(await voterRewardsV5.getGMReward(3, voter1.address)).to.equal(ethers.parseEther("17793.594306049822064056"));
expect(await voterRewardsV5.getGMReward(3, voter2.address)).to.equal(ethers.parseEther("222419.928825622775800711"));
expect(await voterRewardsV5.getGMReward(3, voter3.address)).to.equal(ethers.parseEther("9786.476868327402135231"));
/**
*
* There is 2000000 in the generic pool for round 3
*
* Rewards distribution should no longer be affected by GM as that comes from standalone pool
*
* Cycle to total = 94.868329803
* voter 1 = 31.62 / 94.868329803 * 2000000 = 666666.66 B3TR
* voter 2 = 31.62 / 94.868329803 * 2000000 = 666666.66 B3TR
* voter 3 = 31.62 / 94.868329803 * 2000000 = 666666.66 B3TR
*/
expect(await voterRewardsV5.cycleToTotal(3)).to.equal(ethers.parseEther("94.868329803")); // Total weighted votes (31.62 * 3)
expect(await voterRewardsV5.cycleToVoterToTotal(3, voter1)).to.equal(ethers.parseEther("31.622776601")); // Voter 1 weighted vote
expect(await voterRewardsV5.cycleToVoterToTotal(3, voter2)).to.equal(ethers.parseEther("31.622776601")); // Voter 2 weighted vote
expect(await voterRewardsV5.cycleToVoterToTotal(3, voter3)).to.equal(ethers.parseEther("31.622776601")); // Voter 3 weighted vote
// Wait for next cycle
await waitForNextCycle(emissionsLatest);
// Distribute emissions
await emissionsLatest.connect(minterAccount).distribute();
// Claim Round 3 rewards - verify GM rewards functionality works
await expect(voterRewardsV5.connect(voter1).claimReward(3, voter1.address))
.to.emit(voterRewardsV5, "RewardClaimedV2")
.withArgs(3, voter1.address, 666666666666666666666666n, 17793594306049822064056n);
await expect(voterRewardsV5.connect(voter2).claimReward(3, voter2.address))
.to.emit(voterRewardsV5, "RewardClaimedV2")
.withArgs(3, voter2.address, 666666666666666666666666n, 222419928825622775800711n);
await expect(voterRewardsV5.connect(voter3).claimReward(3, voter3.address))
.to.emit(voterRewardsV5, "RewardClaimedV2")
.withArgs(3, voter3.address, 666666666666666666666666n, 9786476868327402135231n);
// ========================================
// ROUND 4: GM MULTIPLIER UPDATES
// ========================================
// Can update GM multipliers, but it wont take effect until next cycle
await voterRewardsV5.setLevelToMultiplier(2, 1000);
await voterRewardsV5.setLevelToMultiplier(5, 2000);
// Multiplier is set to latest update to level
await expect(voterRewardsV5.setLevelToMultiplier(2, 50))
.to.emit(voterRewardsV5, "LevelToMultiplierPending")
.withArgs(2, 50);
const roundId4 = await xAllocationVoting.currentRoundId();
// Vote on apps for the fourth round
await voteOnApps([app1, app2], [voter1, voter2, voter3], [
[ethers.parseEther("0"), ethers.parseEther("1000")], // Voter 1 votes 1000 for app2
[ethers.parseEther("500"), ethers.parseEther("500")], // Voter 2 votes 500 for app1 and 500 for app2
[ethers.parseEther("500"), ethers.parseEther("500")], // Voter 3 votes 500 for app1 and 500 for app2
], roundId4, // Fourth round
xAllocationVoting, veBetterPassport);
// cycle to GM total should be same as before
expect(await voterRewardsV5.cycleToTotalGMWeight(4)).to.equal(2810n);
expect(await voterRewardsV5.cycleToVoterToGMWeight(4, voter1)).to.equal(200n);
expect(await voterRewardsV5.cycleToVoterToGMWeight(4, voter2)).to.equal(2500n);
expect(await voterRewardsV5.cycleToVoterToGMWeight(4, voter3)).to.equal(110n);
await waitForNextCycle(emissionsLatest);
await emissionsLatest.connect(minterAccount).distribute();
// ========================================
// ROUND 5: UPDATED GM MULTIPLIERS
// ========================================
// GM Multiplier should be updated as of this cycle
const roundId5 = await xAllocationVoting.currentRoundId();
await voteOnApps([app1, app2], [voter1, voter2, voter3], [
[ethers.parseEther("0"), ethers.parseEther("1000")], // Voter 1 votes 1000 for app2
[ethers.parseEther("500"), ethers.parseEther("500")], // Voter 2 votes 500 for app1 and 500 for app2
[ethers.parseEther("500"), ethers.parseEther("500")], // Voter 3 votes 500 for app1 and 500 for app2
], roundId5, // Fifth round
xAllocationVoting, veBetterPassport);
expect(await voterRewardsV5.cycleToVoterToGMWeight(5, voter1)).to.equal(2000n);
expect(await voterRewardsV5.cycleToVoterToGMWeight(5, voter2)).to.equal(2500n);
expect(await voterRewardsV5.cycleToVoterToGMWeight(5, voter3)).to.equal(50n);
// ========================================
// UPGRADE TO V6 (RELAYER FEES)
// ========================================
// Continue with V6 upgrade and relayer fee functionality testing
// Deploy RelayerRewardsPool
const relayerRewardsPool = (await deployProxy("RelayerRewardsPool", [
owner.address,
owner.address,
await b3tr.getAddress(),
await emissionsLatest.getAddress(),
await xAllocationVoting.getAddress(),
]));
// Store V5 storage state before V6 upgrade
const initialSlotV6 = BigInt("0x114e7ffaaf205d38cd05b17b56f3357806ef2ce889cb4748445ae91cdfc37c00");
let storageSlotsV5 = [];
for (let i = initialSlotV6; i < initialSlotV6 + BigInt(20); i++) {
storageSlotsV5.push(await ethers.provider.getStorage(await voterRewardsV5.getAddress(), i));
}
// Upgrade from V5 to V6 (will be updated after XAllocationVoting upgrade)
const voterRewardsV6 = (await upgradeProxy("VoterRewardsV5", "VoterRewards", await voterRewardsV5.getAddress(), [await xAllocationVoting.getAddress(), await relayerRewardsPool.getAddress()], {
version: 6,
}));
// Verify storage compatibility after V6 upgrade
let storageSlotsV6 = [];
for (let i = initialSlotV6; i < initialSlotV6 + BigInt(20); i++) {
storageSlotsV6.push(await ethers.provider.getStorage(await voterRewardsV6.getAddress(), i));
}
for (let i = 0; i < storageSlotsV5.length; i++) {
if (storageSlotsV5[i] !== "0x0000000000000000000000000000000000000000000000000000000000000000") {
expect(storageSlotsV5[i]).to.equal(storageSlotsV6[i], `V6 upgrade: Storage slot ${i} should be preserved`);
}
}
// Verify V6 initialization
expect(await voterRewardsV6.version()).to.equal("7");
expect(await voterRewardsV6.relayerRewardsPool()).to.equal(await relayerRewardsPool.getAddress());
// Test V6 relayer fee functionality with real voting round
const relayer1 = otherAccounts[5];
// Register relayer in RelayerRewardsPool
await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address);
// CRITICAL: Update emissions to distribute rewards to our upgraded VoterRewards V6
await emissionsLatest.connect(owner).setVote2EarnAddress(await voterRewardsV6.getAddress());
await upgradeProxy("B3TRGovernorV6", "B3TRGovernor", await governor.getAddress(), [], {
version: 10,
libraries: {
GovernorClockLogic: await governorClockLogicLib.getAddress(),
GovernorConfigurator: await governorConfiguratorLib.getAddress(),
GovernorDepositLogic: await governorDepositLogicLib.getAddress(),
GovernorFunctionRestrictionsLogic: await governorFunctionRestrictionsLogicLib.getAddress(),
GovernorProposalLogic: await governorProposalLogicLib.getAddress(),
GovernorQuorumLogic: await governorQuorumLogicLib.getAddress(),
GovernorStateLogic: await governorStateLogicLib.getAddress(),
GovernorVotesLogic: await governorVotesLogicLib.getAddress(),
},
});
// Deploy XAllocationVoting libraries
const { xAllocationVotingLibraries: xAllocVotingLibraries } = await import("../../scripts/libraries/xAllocationVotingLibraries");
const xAllocLibs = await xAllocVotingLibraries();
// Upgrade XAllocationVoting to latest
const xAllocationVotingV8 = (await upgradeProxy("XAllocationVotingV6", "XAllocationVoting", await xAllocationVoting.getAddress(), [], {
version: 9,
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(),
},
}));
await xAllocationVotingV8
.connect(owner)
.grantRole(await xAllocationVotingV8.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address);
await xAllocationVotingV8.connect(owner).setB3TRGovernor(await governor.getAddress());
await xAllocationVotingV8.connect(owner).setRelayerRewardsPoolAddress(await relayerRewardsPool.getAddress());
await relayerRewardsPool
.connect(owner)
.grantRole(await relayerRewardsPool.POOL_ADMIN_ROLE(), await xAllocationVotingV8.getAddress());
console.log("Upgraded XAllocationVoting to version:", await xAllocationVotingV8.version());
// Update VoterRewards V6 to use the upgraded XAllocationVoting
await voterRewardsV6.connect(owner).setXAllocationVoting(await xAllocationVotingV8.getAddress());
// Grant VOTE_REGISTRAR_ROLE to the upgraded XAllocationVoting
await voterRewardsV6
.connect(owner)
.grantRole(await voterRewardsV6.VOTE_REGISTRAR_ROLE(), await xAllocationVotingV8.getAddress());
// Enable auto-voting for voter1
await xAllocationVotingV8.connect(voter1).setUserVotingPreferences([app1]);
await xAllocationVotingV8.connect(voter1).toggleAutoVoting(voter1.address);
// ========================================
// ROUND 6: V6 TESTING WITH RELAYER FEES
// ========================================
// Start new round for V6 testing
await waitForNextCycle(emissionsLatest);
await emissionsLatest.connect(minterAccount).distribute();
const roundIdV6 = await xAllocationVotingV8.currentRoundId();
// Cast votes: voter1 via relayer (auto-voting), voter2 manually
await xAllocationVotingV8.connect(relayer1).castVoteOnBehalfOf(voter1.address, roundIdV6);
await xAllocationVotingV8.connect(voter2).castVote(roundIdV6, [app1], [ethers.parseEther("1000")]);
// Wait for next cycle and distribute to make rewards available
await waitForNextCycle(emissionsLatest);
await emissionsLatest.connect(minterAccount).distribute();
// Test V6 reward calculations and track rewards
const voter1Reward = await voterRewardsV6.getReward(roundIdV6, voter1.address);
const voter1GMReward = await voterRewardsV6.getGMReward(roundIdV6, voter1.address);
const voter1Fee = await voterRewardsV6.getRelayerFee(roundIdV6, voter1.address);
const voter2Reward = await voterRewardsV6.getReward(roundIdV6, voter2.address);
const voter2Fee = await voterRewardsV6.getRelayerFee(roundIdV6, voter2.address);
// Verify V6 rewards are calculated correctly
expect(voter1Reward).to.be.gt(0, "V6: Auto-voting user should have rewards");
expect(voter2Reward).to.be.gt(0, "V6: Manual voting user should have rewards");
// Verify V6 fee logic: auto-voting user has fees, manual voting user doesn't
expect(voter1Fee).to.be.gt(0, "V6: Auto-voting user should have relayer fee");
expect(voter2Fee).to.equal(0, "V6: Manual voting user should have zero fee");
// Verify V6 fee calculation matches RelayerRewardsPool
const rawTotalReward1 = voter1Reward + voter1GMReward + voter1Fee;
const expectedFee1 = await relayerRewardsPool.calculateRelayerFee(rawTotalReward1);
expect(voter1Fee).to.equal(expectedFee1, "V6: Fee should match RelayerRewardsPool calculation");
// Verify V6 fee cap logic
const feePercentage = await relayerRewardsPool.getRelayerFeePercentage();
const feeDenominator = await relayerRewardsPool.getRelayerFeeDenominator();
const feeCap = await relayerRewardsPool.getFeeCap();
const calculatedFee = (rawTotalReward1 * feePercentage) / feeDenominator;
const actualFee = calculatedFee > feeCap ? feeCap : calculatedFee;
expect(voter1Fee).to.equal(actualFee, "V6: Fee should respect cap");
// ========================================
// FINAL STATE INTEGRITY VERIFICATION
// ========================================
// Verify V6 contract is functional and state is intact
expect(await voterRewardsV6.version()).to.equal("7", "V7 contract should report correct version");
// Verify V6 fee logic works correctly (core V6 functionality)
expect(voter1Fee).to.be.gt(0, "V6: Auto-voting user should have relayer fees");
expect(voter2Fee).to.equal(0, "V6: Manual voting user should have zero fees");
// Verify V6 rewards are calculated correctly
expect(voter1Reward).to.be.gt(0, "V6: Auto-voting user should have rewards");
expect(voter2Reward).to.be.gt(0, "V6: Manual voting user should have rewards");
// Verify historical data is still accessible (state preservation check)
expect(await voterRewardsV6.getReward(1, voter1.address)).to.equal(0, "Round 1 rewards should be claimed (zero)");
expect(await voterRewardsV6.getReward(2, voter1.address)).to.equal(0, "Round 2 rewards should be claimed (zero)");
expect(await voterRewardsV6.getReward(3, voter1.address)).to.equal(0, "Round 3 rewards should be claimed (zero)");
// Verify contract can still query historical GM data
expect(await voterRewardsV6.getGMReward(3, voter1.address)).to.equal(0, "Round 3 GM rewards should be claimed (zero)");
expect(await voterRewardsV6.getGMReward(3, voter2.address)).to.equal(0, "Round 3 GM rewards should be claimed (zero)");
expect(await voterRewardsV6.getGMReward(3, voter3.address)).to.equal(0, "Round 3 GM rewards should be claimed (zero)");
// Verify historical cycle data is still accessible (proves state preservation works functionally)
expect(await voterRewardsV6.cycleToTotal(1)).to.be.gt(0, "Round 1 cycle total should be preserved");
expect(await voterRewardsV6.cycleToTotal(2)).to.be.gt(0, "Round 2 cycle total should be preserved");
expect(await voterRewardsV6.cycleToTotal(3)).to.be.gt(0, "Round 3 cycle total should be preserved");
// Verify specific historical values are preserved
expect(await voterRewardsV6.cycleToTotalGMWeight(3)).to.equal(2810n, "Round 3 GM weight should be preserved");
});
});