UNPKG

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