@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
789 lines (788 loc) • 159 kB
JavaScript
import { ethers } from "hardhat";
import { expect } from "chai";
import { ZERO_ADDRESS, bootstrapEmissions, calculateBaseAllocationOffChain, calculateUnallocatedAppAllocationOffChain, calculateVariableAppAllocationOffChain, catchRevert, getOrDeployContractInstances, getVot3Tokens, moveToCycle, startNewAllocationRound, waitForRoundToEnd, } from "./helpers";
import { describe, it, before } from "mocha";
import { getImplementationAddress } from "@openzeppelin/upgrades-core";
import { createLocalConfig } from "@repo/config/contracts/envs/local";
import { deployAndUpgrade, deployProxy, upgradeProxy } from "../scripts/helpers";
import { endorseApp } from "./helpers/xnodes";
describe("X-Allocation Pool - @shard13", async function () {
// Environment params
let creator1;
let creator2;
before(async function () {
const { creators } = await getOrDeployContractInstances({ forceDeploy: true });
creator1 = creators[0];
creator2 = creators[1];
});
describe("Deployment", async function () {
it("Contract is correctly initialized", async function () {
const { xAllocationPool, owner, x2EarnApps, emissions, b3tr, treasury } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.unallocatedFundsReceiver()).to.eql(await treasury.getAddress());
expect(await xAllocationPool.b3tr()).to.eql(await b3tr.getAddress());
expect(await xAllocationPool.emissions()).to.eql(await emissions.getAddress());
expect(await xAllocationPool.x2EarnApps()).to.eql(await x2EarnApps.getAddress());
const DEFAULT_ADMIN_ROLE = await xAllocationPool.DEFAULT_ADMIN_ROLE();
const UPGRADER_ROLE = await xAllocationPool.UPGRADER_ROLE();
expect(await xAllocationPool.hasRole(DEFAULT_ADMIN_ROLE, owner.address)).to.eql(true);
expect(await xAllocationPool.hasRole(UPGRADER_ROLE, owner.address)).to.eql(true);
});
it("Should revert if admin is set to zero address in initilisation", async () => {
const config = createLocalConfig();
const { owner, b3tr, treasury, x2EarnApps, x2EarnRewardsPool } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
await expect(deployProxy("XAllocationPoolV1", [
ZERO_ADDRESS,
owner.address,
owner.address,
await b3tr.getAddress(),
await treasury.getAddress(),
await x2EarnApps.getAddress(),
await x2EarnRewardsPool.getAddress(),
])).to.be.reverted;
});
});
describe("Contract upgradeablity", () => {
it("Admin should be able to upgrade the contract", async function () {
const { xAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
// Deploy the implementation contract
const Contract = await ethers.getContractFactory("XAllocationPool");
const implementation = await Contract.deploy();
await implementation.waitForDeployment();
const currentImplAddress = await getImplementationAddress(ethers.provider, await xAllocationPool.getAddress());
const UPGRADER_ROLE = await xAllocationPool.UPGRADER_ROLE();
expect(await xAllocationPool.hasRole(UPGRADER_ROLE, owner.address)).to.eql(true);
await expect(xAllocationPool.connect(owner).upgradeToAndCall(await implementation.getAddress(), "0x")).to.not.be
.reverted;
const newImplAddress = await getImplementationAddress(ethers.provider, await xAllocationPool.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 { xAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
// Deploy the implementation contract
const Contract = await ethers.getContractFactory("XAllocationPool");
const implementation = await Contract.deploy();
await implementation.waitForDeployment();
const currentImplAddress = await getImplementationAddress(ethers.provider, await xAllocationPool.getAddress());
const UPGRADER_ROLE = await xAllocationPool.UPGRADER_ROLE();
expect(await xAllocationPool.hasRole(UPGRADER_ROLE, otherAccount.address)).to.eql(false);
await expect(xAllocationPool.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")).to
.be.reverted;
const newImplAddress = await getImplementationAddress(ethers.provider, await xAllocationPool.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 { xAllocationPool, owner, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
// Deploy the implementation contract
const Contract = await ethers.getContractFactory("XAllocationPool");
const implementation = await Contract.deploy();
await implementation.waitForDeployment();
const currentImplAddress = await getImplementationAddress(ethers.provider, await xAllocationPool.getAddress());
const UPGRADER_ROLE = await xAllocationPool.UPGRADER_ROLE();
expect(await xAllocationPool.hasRole(UPGRADER_ROLE, otherAccount.address)).to.eql(false);
await expect(xAllocationPool.connect(owner).grantRole(UPGRADER_ROLE, otherAccount.address)).to.not.be.reverted;
await expect(xAllocationPool.connect(owner).revokeRole(UPGRADER_ROLE, owner.address)).to.not.be.reverted;
await expect(xAllocationPool.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")).to
.not.be.reverted;
const newImplAddress = await getImplementationAddress(ethers.provider, await xAllocationPool.getAddress());
expect(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase());
expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase());
});
it("Cannot deploy contract with zero address", async function () {
const { b3tr, treasury, owner, x2EarnApps } = await getOrDeployContractInstances({
forceDeploy: false,
});
await expect(deployProxy("XAllocationPoolV1", [
owner.address,
owner.address,
owner.address,
ZERO_ADDRESS,
await treasury.getAddress(),
await x2EarnApps.getAddress(),
owner.address,
])).to.be.reverted;
await expect(deployProxy("XAllocationPoolV1", [
owner.address,
owner.address,
owner.address,
await b3tr.getAddress(),
ZERO_ADDRESS,
await x2EarnApps.getAddress(),
owner.address,
])).to.be.reverted;
await expect(deployProxy("XAllocationPoolV1", [
owner.address,
owner.address,
owner.address,
await b3tr.getAddress(),
await treasury.getAddress(),
ZERO_ADDRESS,
owner.address,
])).to.be.reverted;
await expect(deployProxy("XAllocationPoolV1", [
owner.address,
owner.address,
owner.address,
await b3tr.getAddress(),
await treasury.getAddress(),
owner.address,
ZERO_ADDRESS,
])).to.be.reverted;
});
it("Cannot initilize twice", async function () {
const { owner, b3tr, treasury, x2EarnApps, x2EarnRewardsPool } = await getOrDeployContractInstances({
forceDeploy: false,
});
// Deploy XAllocationPool
const xAllocationPoolV1 = (await deployProxy("XAllocationPoolV1", [
owner.address,
owner.address,
owner.address,
await b3tr.getAddress(),
await treasury.getAddress(),
await x2EarnApps.getAddress(),
await x2EarnRewardsPool.getAddress(),
]));
await catchRevert(xAllocationPoolV1.initialize(owner.address, owner.address, owner.address, await b3tr.getAddress(), await treasury.getAddress(), await x2EarnApps.getAddress(), await x2EarnRewardsPool.getAddress()));
});
it("Should return correct version of the contract", async () => {
const { xAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.version()).to.equal("7");
});
it("Should not have state conflict after upgrading to V6", async () => {
const config = createLocalConfig();
config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 100;
config.X_ALLOCATION_POOL_BASE_ALLOCATION_PERCENTAGE = 0;
config.INITIAL_X_ALLOCATION = 10000n;
config.X_ALLOCATION_VOTING_QUORUM_PERCENTAGE = 0;
config.EMISSIONS_CYCLE_DURATION = 60; // Increase cycle duration
const { otherAccounts, owner, b3tr, x2EarnRewardsPool, emissions, x2EarnApps, xAllocationVoting, treasury, veBetterPassport, } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
// Deploy XAllocationPool
const xAllocationPoolV1 = (await deployAndUpgrade(["XAllocationPoolV1", "XAllocationPoolV2", "XAllocationPoolV3", "XAllocationPoolV4", "XAllocationPoolV5"], [
[
owner.address,
owner.address,
owner.address,
await b3tr.getAddress(),
await treasury.getAddress(),
await x2EarnApps.getAddress(),
await x2EarnRewardsPool.getAddress(),
],
[],
[],
[],
[],
], {
versions: [undefined, 2, 3, 4, 5],
}));
await xAllocationPoolV1.connect(owner).setXAllocationVotingAddress(await xAllocationVoting.getAddress());
await xAllocationPoolV1.connect(owner).setEmissionsAddress(await emissions.getAddress());
// Bootstrap emissions
await bootstrapEmissions();
otherAccounts.forEach(async (account) => {
await veBetterPassport.whitelist(account.address);
await getVot3Tokens(account, "10000");
});
await veBetterPassport.toggleCheck(1);
//Add apps
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app"));
const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2"));
const app3Id = ethers.keccak256(ethers.toUtf8Bytes("My app #3"));
await x2EarnApps
.connect(owner)
.submitApp(otherAccounts[3].address, otherAccounts[3].address, "My app", "metadataURI");
await x2EarnApps
.connect(creator1)
.submitApp(otherAccounts[4].address, otherAccounts[4].address, "My app #2", "metadataURI");
await x2EarnApps
.connect(creator2)
.submitApp(otherAccounts[5].address, otherAccounts[5].address, "My app #3", "metadataURI");
await endorseApp(app1Id, otherAccounts[1]);
await endorseApp(app2Id, otherAccounts[2]);
await endorseApp(app3Id, otherAccounts[3]);
//Start allocation round
const round1 = await startNewAllocationRound();
// Vote
await xAllocationVoting
.connect(otherAccounts[1])
.castVote(round1, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[2])
.castVote(round1, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[3])
.castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[4])
.castVote(round1, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[5])
.castVote(round1, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]);
await waitForRoundToEnd(round1);
const app1round1Earnings = await xAllocationPoolV1.roundEarnings(round1, app1Id);
const app2round1Earnings = await xAllocationPoolV1.roundEarnings(round1, app2Id);
const app3round1Earnings = await xAllocationPoolV1.roundEarnings(round1, app3Id);
expect(app1round1Earnings[0]).to.eql(1144n);
expect(app2round1Earnings[0]).to.eql(5993n);
expect(app3round1Earnings[0]).to.eql(2861n);
//Start allocation round
const round2 = await startNewAllocationRound();
// Vote
await xAllocationVoting
.connect(otherAccounts[1])
.castVote(round2, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[2])
.castVote(round2, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[3])
.castVote(round2, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[4])
.castVote(round2, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[5])
.castVote(round2, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]);
await waitForRoundToEnd(round2);
// start new round
const app1round2Earnings = await xAllocationPoolV1.roundEarnings(round2, app1Id);
const app2round2Earnings = await xAllocationPoolV1.roundEarnings(round2, app2Id);
const app3round2Earnings = await xAllocationPoolV1.roundEarnings(round2, app3Id);
expect(app1round2Earnings[0]).to.eql(1144n);
expect(app2round2Earnings[0]).to.eql(5993n);
expect(app3round2Earnings[0]).to.eql(2861n);
let storageSlots = [];
const initialSlot = BigInt("0xba46220259871765522240056f76631a28aa19c5092d6dd51d6b858b4ebcb300"); // Slot 0 of VoterRewards
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlots.push(await ethers.provider.getStorage(await xAllocationPoolV1.getAddress(), i));
}
storageSlots = storageSlots.filter(slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000"); // removing empty slots
const xAllocationPool = (await upgradeProxy("XAllocationPoolV5", "XAllocationPool", await xAllocationPoolV1.getAddress(), [], {
version: 6,
}));
let storageSlotsAfter = [];
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlotsAfter.push(await ethers.provider.getStorage(await xAllocationPool.getAddress(), i));
}
storageSlotsAfter = storageSlotsAfter.filter(slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000"); // removing empty slots
// Check if storage slots are the same after upgrade
for (let i = 0; i < storageSlots.length; i++) {
expect(storageSlots[i]).to.equal(storageSlotsAfter[i]);
}
otherAccounts.forEach(async (account) => {
await getVot3Tokens(account, "10000");
});
//Start allocation round
const round3 = await startNewAllocationRound();
// Check Quadratic Funding is on
expect(await xAllocationPool.isQuadraticFundingDisabledForCurrentRound()).to.eql(false);
// Vote
await xAllocationVoting
.connect(otherAccounts[1])
.castVote(round3, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[2])
.castVote(round3, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[3])
.castVote(round3, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[4])
.castVote(round3, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
// Turn off quadratic funding mid round
await xAllocationPool.connect(owner).toggleQuadraticFunding();
await xAllocationVoting
.connect(otherAccounts[5])
.castVote(round3, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]);
await waitForRoundToEnd(round3);
const app1round3Earnings = await xAllocationPool.roundEarnings(round3, app1Id);
const app2round3Earnings = await xAllocationPool.roundEarnings(round3, app2Id);
const app3round3Earnings = await xAllocationPool.roundEarnings(round3, app3Id);
expect(app1round3Earnings[0]).to.eql(1144n);
expect(app2round3Earnings[0]).to.eql(5993n);
expect(app3round3Earnings[0]).to.eql(2861n); // remains quadratic
//Start allocation round
const round4 = await startNewAllocationRound();
// Check Quadratic Funding is off
expect(await xAllocationPool.isQuadraticFundingDisabledForCurrentRound()).to.eql(true);
expect(await xAllocationPool.isQuadraticFundingDisabledForRound(round4)).to.eql(true);
// Vote
await xAllocationVoting
.connect(otherAccounts[1])
.castVote(round4, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[2])
.castVote(round4, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[3])
.castVote(round4, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[4])
.castVote(round4, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[5])
.castVote(round4, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]);
await waitForRoundToEnd(round4);
// start new round
const app1round4Earnings = await xAllocationPool.roundEarnings(round4, app1Id);
const app2round4Earnings = await xAllocationPool.roundEarnings(round4, app2Id);
const app3round4Earnings = await xAllocationPool.roundEarnings(round4, app3Id);
/*
app1 percentage = 1000 / 3100 = 32.25% (No cap)
app2 percentage = 1600 / 3100 = 51.61% (No cap)
app3 percentage = 500 / 3100 = 16.12%
*/
expect(app1round4Earnings[0]).to.eql(3225n);
expect(app2round4Earnings[0]).to.eql(5161n);
expect(app3round4Earnings[0]).to.eql(1612n);
// Check that round earings from round 1 are still the same after toggle
const app1round1Earnings1 = await xAllocationPool.roundEarnings(round1, app1Id);
const app2round1Earnings2 = await xAllocationPool.roundEarnings(round1, app2Id);
const app3round1Earnings3 = await xAllocationPool.roundEarnings(round1, app3Id);
// Check that round earings from round 1 are still the same after toggle
expect(app1round1Earnings1[0]).to.eql(1144n);
expect(app2round1Earnings2[0]).to.eql(5993n);
expect(app3round1Earnings3[0]).to.eql(2861n);
// Capture storage slots before upgrading to V7
let storageSlotsBeforeV7 = [];
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlotsBeforeV7.push(await ethers.provider.getStorage(await xAllocationPool.getAddress(), i));
}
storageSlotsBeforeV7 = storageSlotsBeforeV7.filter(slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000"); // removing empty slots
// Upgrade to V7
const xAllocationPoolV7 = (await upgradeProxy("XAllocationPoolV6", "XAllocationPool", await xAllocationPool.getAddress(), [[], []], {
version: 7,
}));
let storageSlotsAfterV7 = [];
for (let i = initialSlot; i < initialSlot + BigInt(100); i++) {
storageSlotsAfterV7.push(await ethers.provider.getStorage(await xAllocationPoolV7.getAddress(), i));
}
storageSlotsAfterV7 = storageSlotsAfterV7.filter(slot => slot !== "0x0000000000000000000000000000000000000000000000000000000000000000"); // removing empty slots
// Check if storage slots are the same after upgrade to V7
for (let i = 0; i < storageSlotsBeforeV7.length; i++) {
expect(storageSlotsBeforeV7[i]).to.equal(storageSlotsAfterV7[i]);
}
// Verify all historical round earnings are still intact after V7 upgrade
const app1round1EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round1, app1Id);
const app2round1EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round1, app2Id);
const app3round1EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round1, app3Id);
expect(app1round1EarningsAfterV7[0]).to.eql(1144n);
expect(app2round1EarningsAfterV7[0]).to.eql(5993n);
expect(app3round1EarningsAfterV7[0]).to.eql(2861n);
const app1round2EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round2, app1Id);
const app2round2EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round2, app2Id);
const app3round2EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round2, app3Id);
expect(app1round2EarningsAfterV7[0]).to.eql(1144n);
expect(app2round2EarningsAfterV7[0]).to.eql(5993n);
expect(app3round2EarningsAfterV7[0]).to.eql(2861n);
const app1round3EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round3, app1Id);
const app2round3EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round3, app2Id);
const app3round3EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round3, app3Id);
expect(app1round3EarningsAfterV7[0]).to.eql(1144n);
expect(app2round3EarningsAfterV7[0]).to.eql(5993n);
expect(app3round3EarningsAfterV7[0]).to.eql(2861n);
const app1round4EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round4, app1Id);
const app2round4EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round4, app2Id);
const app3round4EarningsAfterV7 = await xAllocationPoolV7.roundEarnings(round4, app3Id);
expect(app1round4EarningsAfterV7[0]).to.eql(3225n);
expect(app2round4EarningsAfterV7[0]).to.eql(5161n);
expect(app3round4EarningsAfterV7[0]).to.eql(1612n);
// Verify quadratic funding state persists after V7 upgrade
expect(await xAllocationPoolV7.isQuadraticFundingDisabledForCurrentRound()).to.eql(true);
expect(await xAllocationPoolV7.isQuadraticFundingDisabledForRound(round4)).to.eql(true);
// Run a new round after V7 upgrade to verify functionality
otherAccounts.forEach(async (account) => {
await getVot3Tokens(account, "10000");
});
const round5 = await startNewAllocationRound();
// Vote in round 5
await xAllocationVoting
.connect(otherAccounts[1])
.castVote(round5, [app2Id, app3Id], [ethers.parseEther("900"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[2])
.castVote(round5, [app2Id, app3Id], [ethers.parseEther("500"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[3])
.castVote(round5, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[4])
.castVote(round5, [app2Id, app3Id], [ethers.parseEther("100"), ethers.parseEther("100")]);
await xAllocationVoting
.connect(otherAccounts[5])
.castVote(round5, [app1Id, app3Id], [ethers.parseEther("1000"), ethers.parseEther("100")]);
await waitForRoundToEnd(round5);
const app1round5Earnings = await xAllocationPoolV7.roundEarnings(round5, app1Id);
const app2round5Earnings = await xAllocationPoolV7.roundEarnings(round5, app2Id);
const app3round5Earnings = await xAllocationPoolV7.roundEarnings(round5, app3Id);
// Should use linear calculation since quadratic funding is disabled
expect(app1round5Earnings[0]).to.eql(3225n);
expect(app2round5Earnings[0]).to.eql(5161n);
expect(app3round5Earnings[0]).to.eql(1612n);
});
});
describe("Settings", async function () {
describe("Unallocated funds receiver address", async function () {
it("Admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set the unallocated funds receiver address", async function () {
const { xAllocationPool, owner, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.hasRole(await xAllocationPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address)).to.eql(true);
const newTreasuryAddress = otherAccount.address;
await xAllocationPool.connect(owner).setUnallocatedFundsReceiverAddress(newTreasuryAddress);
const unallocatedFundsReceiver = await xAllocationPool.unallocatedFundsReceiver();
expect(unallocatedFundsReceiver).to.eql(newTreasuryAddress);
});
it("Only admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set the unallocated funds receiver address", async function () {
const { xAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.hasRole(await xAllocationPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), otherAccount.address)).to.eql(false);
const newTreasuryAddress = otherAccount.address;
await expect(xAllocationPool.connect(otherAccount).setUnallocatedFundsReceiverAddress(newTreasuryAddress)).to.be
.reverted;
});
it("Cannot set the unallocated funds receiver address to zero address", async function () {
const { xAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newTreasuryAddress = ZERO_ADDRESS;
await expect(xAllocationPool.connect(owner).setUnallocatedFundsReceiverAddress(newTreasuryAddress)).to.be
.reverted;
});
});
describe("Emissions address", async function () {
it("Admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set emissions contract address", async function () {
const { xAllocationPool, owner, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.hasRole(await xAllocationPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address)).to.eql(true);
const newEmissionsAddress = otherAccount.address;
await xAllocationPool.connect(owner).setEmissionsAddress(newEmissionsAddress);
const emissionsAddress = await xAllocationPool.emissions();
expect(emissionsAddress).to.eql(newEmissionsAddress);
});
it("Only admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set emissions contract address", async function () {
const { xAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.hasRole(await xAllocationPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), otherAccount.address)).to.eql(false);
const newEmissionsAddress = otherAccount.address;
await expect(xAllocationPool.connect(otherAccount).setEmissionsAddress(newEmissionsAddress)).to.be.reverted;
});
it("Cannot set emissions contract address to zero address", async function () {
const { xAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newEmissionsAddress = ZERO_ADDRESS;
await expect(xAllocationPool.connect(owner).setEmissionsAddress(newEmissionsAddress)).to.be.reverted;
});
it("Cannot calculate emissions amount if emissions contract is not set", async function () {
const { owner } = await getOrDeployContractInstances({
forceDeploy: false,
});
const xAllocationPool = (await deployAndUpgrade(["XAllocationPoolV1", "XAllocationPoolV2", "XAllocationPoolV3", "XAllocationPoolV4", "XAllocationPool"], [
[owner.address, owner.address, owner.address, owner.address, owner.address, owner.address, owner.address],
[],
[],
[],
[],
], {
versions: [undefined, 2, 3, 4, 5],
}));
await xAllocationPool.setXAllocationVotingAddress(owner.address);
expect(await xAllocationPool.emissions()).to.eql(ZERO_ADDRESS);
await expect(xAllocationPool.baseAllocationAmount(1)).to.be.reverted;
});
});
describe("XAllocationVoting address", async function () {
it("Admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set xAllocationVoting contract address", async function () {
const { xAllocationPool, owner, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.hasRole(await xAllocationPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address)).to.eql(true);
const newXAllocationVotingAddress = otherAccount.address;
await xAllocationPool.connect(owner).setXAllocationVotingAddress(newXAllocationVotingAddress);
const xAllocationVotingAddress = await xAllocationPool.xAllocationVoting();
expect(xAllocationVotingAddress).to.eql(newXAllocationVotingAddress);
});
it("Only admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set xAllocationVoting contract address", async function () {
const { xAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.hasRole(await xAllocationPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), otherAccount.address)).to.eql(false);
const newXAllocationVotingAddress = otherAccount.address;
await expect(xAllocationPool.connect(otherAccount).setXAllocationVotingAddress(newXAllocationVotingAddress)).to
.be.reverted;
});
it("Cannot set xAllocationVoting contract address to zero address", async function () {
const { xAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newXAllocationVotingAddress = ZERO_ADDRESS;
await expect(xAllocationPool.connect(owner).setXAllocationVotingAddress(newXAllocationVotingAddress)).to.be
.reverted;
});
it("Cannot call getAppShares or baseAllocationAmount if xAllocationVoting is not set", async function () {
const { owner } = await getOrDeployContractInstances({
forceDeploy: false,
});
const xAllocationPool = (await deployAndUpgrade(["XAllocationPoolV1", "XAllocationPoolV2", "XAllocationPoolV3", "XAllocationPoolV4", "XAllocationPool"], [
[owner.address, owner.address, owner.address, owner.address, owner.address, owner.address, owner.address],
[],
[],
[],
[],
], {
versions: [undefined, 2, 3, 4, 5],
}));
expect(await xAllocationPool.xAllocationVoting()).to.eql(ZERO_ADDRESS);
await expect(xAllocationPool.baseAllocationAmount(1)).to.be.reverted;
await expect(xAllocationPool.getAppShares(1, ethers.keccak256(ethers.toUtf8Bytes(ZERO_ADDRESS)))).to.be.reverted;
});
});
describe("x2EarnApps address", async function () {
it("Admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set x2EarnApps contract address", async function () {
const { xAllocationPool, owner, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.hasRole(await xAllocationPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), owner.address)).to.eql(true);
const newX2EarnAppsAddress = otherAccount.address;
await xAllocationPool.connect(owner).setX2EarnAppsAddress(newX2EarnAppsAddress);
const x2EarnAppsAddress = await xAllocationPool.x2EarnApps();
expect(x2EarnAppsAddress).to.eql(newX2EarnAppsAddress);
});
it("Only admin with CONTRACTS_ADDRESS_MANAGER_ROLE can set x2EarnApps contract address", async function () {
const { xAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await xAllocationPool.hasRole(await xAllocationPool.CONTRACTS_ADDRESS_MANAGER_ROLE(), otherAccount.address)).to.eql(false);
const newX2EarnAppsAddress = otherAccount.address;
await expect(xAllocationPool.connect(otherAccount).setX2EarnAppsAddress(newX2EarnAppsAddress)).to.be.reverted;
});
it("Cannot set x2EarnApps contract address to zero address", async function () {
const { xAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newX2EarnAppsAddress = ZERO_ADDRESS;
await expect(xAllocationPool.connect(owner).setX2EarnAppsAddress(newX2EarnAppsAddress)).to.be.reverted;
});
});
});
describe("Allocation rewards for x-apps", async function () {
describe("App shares and base allocation", async function () {
it("App can receive a max amount of allocation share and unallocated amount gets sent to treasury", async function () {
const { xAllocationVoting, otherAccounts, owner, xAllocationPool, x2EarnApps, veBetterPassport } = await getOrDeployContractInstances({
forceDeploy: true,
});
// Bootstrap emissions
await bootstrapEmissions();
const voter1 = otherAccounts[1];
await getVot3Tokens(voter1, "1000");
await veBetterPassport.whitelist(voter1.address);
await veBetterPassport.toggleCheck(1);
//Add apps
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app"));
const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2"));
await x2EarnApps
.connect(owner)
.submitApp(otherAccounts[3].address, otherAccounts[3].address, "My app", "metadataURI");
await endorseApp(app1Id, otherAccounts[3]);
await x2EarnApps
.connect(creator1)
.submitApp(otherAccounts[4].address, otherAccounts[4].address, "My app #2", "metadataURI");
await endorseApp(app2Id, otherAccounts[4]);
//Start allocation round
const round1 = await startNewAllocationRound();
// Vote
await xAllocationVoting
.connect(voter1)
.castVote(round1, [app1Id, app2Id], [ethers.parseEther("100"), ethers.parseEther("900")]);
await waitForRoundToEnd(round1);
// expect not to be cupped since it's lower than maxCapPercentage
let app1Shares = await xAllocationPool.getAppShares(round1, app1Id);
expect(app1Shares[0]).to.eql(1000n);
let app2Shares = await xAllocationPool.getAppShares(round1, app2Id);
// should be capped to 20%
let maxCapPercentage = await xAllocationPool.scaledAppSharesCap(round1);
expect(app2Shares[0]).to.eql(maxCapPercentage);
expect(app2Shares[1]).to.eql(7000n); // 100% - baseAllocation(10%) - app1Shares(20%) = 70%
});
it("Every app in the round receives a base allocation", async function () {
const { xAllocationVoting, otherAccounts, owner, xAllocationPool, b3tr, emissions, minterAccount, x2EarnApps } = await getOrDeployContractInstances({
forceDeploy: true,
});
// SEED DATA
const voter1 = otherAccounts[1];
await getVot3Tokens(voter1, "1000");
//Add apps
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app"));
const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2"));
const app1ReceiverAddress = otherAccounts[3].address;
const app2ReceiverAddress = otherAccounts[4].address;
await x2EarnApps.connect(owner).submitApp(app1ReceiverAddress, app1ReceiverAddress, "My app", "metadataURI");
await x2EarnApps
.connect(creator1)
.submitApp(app2ReceiverAddress, app2ReceiverAddress, "My app #2", "metadataURI");
await endorseApp(app1Id, otherAccounts[3]);
await endorseApp(app2Id, otherAccounts[4]);
// Bootstrap emissions
await bootstrapEmissions();
await emissions.connect(minterAccount).start();
//Start allocation round
const round1 = parseInt((await xAllocationVoting.currentRoundId()).toString());
// Nobody votes
await waitForRoundToEnd(round1);
await xAllocationVoting.finalizeRound(round1);
// ENDED SEEDING DATA
// Send 100% to the team instead of x2EarnRewardsPool
await x2EarnApps.connect(owner).setTeamAllocationPercentage(app1Id, 100);
await x2EarnApps.connect(owner).setTeamAllocationPercentage(app2Id, 100);
// CLAIMING
const baseAllocationAmount = await xAllocationPool.baseAllocationAmount(round1);
let app1Revenue = await xAllocationPool.roundEarnings(round1, app1Id);
let app2Revenue = await xAllocationPool.roundEarnings(round1, app2Id);
expect(app1Revenue[0]).to.eql(baseAllocationAmount);
expect(app2Revenue[0]).to.eql(baseAllocationAmount);
let app1Balance = await b3tr.balanceOf(app1ReceiverAddress);
let app2Balance = await b3tr.balanceOf(app2ReceiverAddress);
expect(app1Balance).to.eql(0n);
expect(app2Balance).to.eql(0n);
await xAllocationPool.claim(round1, app1Id);
await xAllocationPool.claim(round1, app2Id);
app1Balance = await b3tr.balanceOf(app1ReceiverAddress);
app2Balance = await b3tr.balanceOf(app2ReceiverAddress);
expect(app1Balance).to.eql(baseAllocationAmount);
expect(app2Balance).to.eql(baseAllocationAmount);
});
it("New app of failed round receives a base allocation even if it was not eligible in previous round", async function () {
const { xAllocationVoting, otherAccounts, owner, veBetterPassport, xAllocationPool, b3tr, emissions, minterAccount, x2EarnApps, } = await getOrDeployContractInstances({
forceDeploy: true,
});
// SEED DATA
const voter1 = otherAccounts[1];
await getVot3Tokens(voter1, "1000");
//Add apps
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app"));
const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2"));
const app1ReceiverAddress = otherAccounts[3].address;
const app2ReceiverAddress = otherAccounts[4].address;
await x2EarnApps.connect(owner).submitApp(app1ReceiverAddress, app1ReceiverAddress, "My app", "metadataURI");
await x2EarnApps
.connect(creator1)
.submitApp(app2ReceiverAddress, app2ReceiverAddress, "My app #2", "metadataURI");
await endorseApp(app1Id, otherAccounts[3]);
await endorseApp(app2Id, otherAccounts[4]);
// Bootstrap emissions
await bootstrapEmissions();
await emissions.connect(minterAccount).start();
await veBetterPassport.whitelist(voter1.address);
await veBetterPassport.toggleCheck(1);
//Start allocation round
const round1 = parseInt((await xAllocationVoting.currentRoundId()).toString());
await xAllocationVoting
.connect(voter1)
.castVote(round1, [app1Id, app2Id], [ethers.parseEther("100"), ethers.parseEther("900")]);
await waitForRoundToEnd(round1);
await xAllocationVoting.finalizeRound(round1);
let state = await xAllocationVoting.state(round1);
// should be succeeded
expect(state).to.eql(2n);
// new emission, new round and new app
const app3Id = ethers.keccak256(ethers.toUtf8Bytes("My app #3"));
const app3ReceiverAddress = otherAccounts[4].address;
await x2EarnApps
.connect(creator2)
.submitApp(app3ReceiverAddress, app3ReceiverAddress, "My app #3", "metadataURI");
await endorseApp(app3Id, otherAccounts[5]);
await x2EarnApps.connect(otherAccounts[4]).setTeamAllocationPercentage(app3Id, 100);
await moveToCycle(3);
const round2 = parseInt((await xAllocationVoting.currentRoundId()).toString());
expect(round2).to.eql(2);
await xAllocationVoting.connect(voter1).castVote(round2, [app3Id], [ethers.parseEther("1")]);
await waitForRoundToEnd(round2);
await xAllocationVoting.finalizeRound(round2);
state = await xAllocationVoting.state(round2);
// should be failed
expect(state).to.eql(1n);
const baseAllocationAmount = await xAllocationPool.baseAllocationAmount(round2);
let round1Votes = await xAllocationVoting.getAppVotes(round1, app3Id);
expect(round1Votes).to.eql(0n);
let round2Votes = await xAllocationVoting.getAppVotes(round2, app3Id);
expect(round2Votes).to.eql(ethers.parseEther("1"));
let app3Revenue = await xAllocationPool.roundEarnings(round2, app3Id);
expect(app3Revenue[0]).to.eql(baseAllocationAmount);
let app3Balance = await b3tr.balanceOf(app3ReceiverAddress);
expect(app3Balance).to.eql(0n);
await xAllocationPool.claim(round2, app3Id);
app3Balance = await b3tr.balanceOf(app3ReceiverAddress);
expect(app3Balance).to.eql(baseAllocationAmount);
});
it("App shares cap and unallocated share of a past round should be checkpointed", async function () {
const { xAllocationVoting, otherAccounts, owner, xAllocationPool, emissions, minterAccount, x2EarnApps, veBetterPassport, } = await getOrDeployContractInstances({
forceDeploy: true,
});
const voter1 = otherAccounts[1];
await getVot3Tokens(voter1, "2000");
//Add apps
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("My app"));
const app2Id = ethers.keccak256(ethers.toUtf8Bytes("My app #2"));
await x2EarnApps
.connect(owner)
.submitApp(otherAccounts[2].address, otherAccounts[2].address, "My app", "metadataURI");
await x2EarnApps
.connect(creator1)
.submitApp(otherAccounts[3].address, otherAccounts[3].address, "My app #2", "metadataURI");
await endorseApp(app1Id, otherAccounts[2]);
await endorseApp(app2Id, otherAccounts[3]);
// Bootstrap emissions
await bootstrapEmissions();
await emissions.connect(minterAccount).start();
await veBetterPassport.whitelist(voter1.address);
await veBetterPassport.toggleCheck(1);
const round1 = await xAllocationVoting.currentRoundId();
// Vote
await xAllocationVoting.connect(voter1).castVote(round1, [app1Id], [ethers.parseEther("1000")]);
await waitForRoundToEnd(Number(round1));
let state = await xAllocationVoting.state(round1);
expect(state).to.eql(BigInt(2));
// Update cap
const GOVERNANCE_ROLE = await xAllocationVoting.GOVERNANCE_ROLE();
await xAllocationVoting.connect(owner).grantRole(GOVERNANCE_ROLE, owner.address);
await xAllocationVoting.connect(owner).setAppSharesCap(50);
await xAllocationVoting.connect(owner).startNewRound();
const round2 = await xAllocationVoting.currentRoundId();
// Vote
await xAllocationVoting.connect(voter1).castVote(round2, [app1Id], [ethers.parseEther("1000")]);
await waitForRoundToEnd(Number(round2));
const expectedBaseAllocationR1 = await calculateBaseAllocationOffChain(Number(round1));
let expectedVariableAllocationR1App1 = await calculateVariableAppAllocationOffChain(Number(round1), app1Id);
const expecteUnallocatedAllocationR1App1 = await calculateUnallocatedAppAllocationOffChain(Number(round1), app1Id);
// should be capped to 20%
let maxCapPercentageR1 = await xAllocationPool.scaledAppSharesCap(round1);
const appSharesR1A1 = await xAllocationPool.getAppShares(round1, app1Id);
expect(appSharesR1A1[0]).to.eql(maxCapPercentageR1);
// Unallocated amount should be 80%
expect(appSharesR1A1[1]).to.eql(8000n); // 100% - appShareCap(20%) = 80%
// should be capped to 50%
let maxCapPercentageR2 = await xAllocationPool.scaledAppSharesCap(round2);
const appSharesR2A1 = await xAllocationPool.getAppShares(round2, app1Id);
expect(appSharesR2A1[0]).to.eql(maxCapPercentageR2);
// Unallocated amount should be 50%