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