UNPKG

@vechain/vebetterdao-contracts

Version:

Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.

733 lines 100 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const hardhat_1 = require("hardhat"); const chai_1 = require("chai"); const helpers_1 = require("./helpers"); const mocha_1 = require("mocha"); const upgrades_core_1 = require("@openzeppelin/upgrades-core"); const local_1 = require("@repo/config/contracts/envs/local"); const helpers_2 = require("../scripts/helpers"); const xnodes_1 = require("./helpers/xnodes"); (0, mocha_1.describe)("DBA Pool - @shard7b", async function () { // Environment params let owner; let otherAccount; let distributor; let upgrader; (0, mocha_1.before)(async function () { const { owner: deployedOwner, otherAccount: deployedOtherAccount, otherAccounts, } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); owner = deployedOwner; otherAccount = deployedOtherAccount; distributor = otherAccounts[0]; upgrader = otherAccounts[1]; }); (0, mocha_1.describe)("Deployment and Initialization", async function () { (0, mocha_1.it)("Contract is correctly initialized", async function () { const { dynamicBaseAllocationPool, x2EarnApps, xAllocationPool, x2EarnRewardsPool, b3tr } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); (0, chai_1.expect)(await dynamicBaseAllocationPool.x2EarnApps()).to.eql(await x2EarnApps.getAddress()); (0, chai_1.expect)(await dynamicBaseAllocationPool.xAllocationPool()).to.eql(await xAllocationPool.getAddress()); (0, chai_1.expect)(await dynamicBaseAllocationPool.x2EarnRewardsPool()).to.eql(await x2EarnRewardsPool.getAddress()); (0, chai_1.expect)(await dynamicBaseAllocationPool.b3tr()).to.eql(await b3tr.getAddress()); (0, chai_1.expect)(await dynamicBaseAllocationPool.distributionStartRound()).to.eql(1n); const DEFAULT_ADMIN_ROLE = await dynamicBaseAllocationPool.DEFAULT_ADMIN_ROLE(); (0, chai_1.expect)(await dynamicBaseAllocationPool.hasRole(DEFAULT_ADMIN_ROLE, owner.address)).to.eql(true); }); (0, mocha_1.it)("Should return correct version", async function () { const { dynamicBaseAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); (0, chai_1.expect)(await dynamicBaseAllocationPool.version()).to.eql("3"); }); (0, mocha_1.it)("Should revert if admin is set to zero address in initialization", async () => { const config = (0, local_1.createLocalConfig)(); const { b3tr, x2EarnApps, xAllocationPool, x2EarnRewardsPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, chai_1.expect)((0, helpers_2.deployProxy)("DBAPool", [ { admin: helpers_1.ZERO_ADDRESS, x2EarnApps: await x2EarnApps.getAddress(), xAllocationPool: await xAllocationPool.getAddress(), x2earnRewardsPool: await x2EarnRewardsPool.getAddress(), b3tr: await b3tr.getAddress(), distributionStartRound: 1, }, ])).to.be.revertedWith("DBAPool: admin is the zero address"); }); (0, mocha_1.it)("Should revert if x2EarnApps is set to zero address in initialization", async () => { const config = (0, local_1.createLocalConfig)(); const { owner, b3tr, xAllocationPool, x2EarnRewardsPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, chai_1.expect)((0, helpers_2.deployProxy)("DBAPool", [ { admin: owner.address, x2EarnApps: helpers_1.ZERO_ADDRESS, xAllocationPool: await xAllocationPool.getAddress(), x2earnRewardsPool: await x2EarnRewardsPool.getAddress(), b3tr: await b3tr.getAddress(), distributionStartRound: 1, }, ])).to.be.revertedWith("DBAPool: x2EarnApps is the zero address"); }); (0, mocha_1.it)("Should revert if xAllocationPool is set to zero address in initialization", async () => { const config = (0, local_1.createLocalConfig)(); const { owner, b3tr, x2EarnApps, x2EarnRewardsPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, chai_1.expect)((0, helpers_2.deployProxy)("DBAPool", [ { admin: owner.address, x2EarnApps: await x2EarnApps.getAddress(), xAllocationPool: helpers_1.ZERO_ADDRESS, x2earnRewardsPool: await x2EarnRewardsPool.getAddress(), b3tr: await b3tr.getAddress(), distributionStartRound: 1, }, ])).to.be.revertedWith("DBAPool: xAllocationPool is the zero address"); }); (0, mocha_1.it)("Should revert if x2earnRewardsPool is set to zero address in initialization", async () => { const config = (0, local_1.createLocalConfig)(); const { owner, b3tr, x2EarnApps, xAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, chai_1.expect)((0, helpers_2.deployProxy)("DBAPool", [ { admin: owner.address, x2EarnApps: await x2EarnApps.getAddress(), xAllocationPool: await xAllocationPool.getAddress(), x2earnRewardsPool: helpers_1.ZERO_ADDRESS, b3tr: await b3tr.getAddress(), distributionStartRound: 1, }, ])).to.be.revertedWith("DBAPool: x2EarnRewardsPool is the zero address"); }); (0, mocha_1.it)("Should revert if b3tr is set to zero address in initialization", async () => { const config = (0, local_1.createLocalConfig)(); const { owner, x2EarnApps, xAllocationPool, x2EarnRewardsPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, chai_1.expect)((0, helpers_2.deployProxy)("DBAPool", [ { admin: owner.address, x2EarnApps: await x2EarnApps.getAddress(), xAllocationPool: await xAllocationPool.getAddress(), x2earnRewardsPool: await x2EarnRewardsPool.getAddress(), b3tr: helpers_1.ZERO_ADDRESS, distributionStartRound: 1, }, ])).to.be.revertedWith("DBAPool: b3tr is the zero address"); }); (0, mocha_1.it)("Should revert if distributionStartRound is zero in initialization", async () => { const config = (0, local_1.createLocalConfig)(); const { owner, b3tr, x2EarnApps, xAllocationPool, x2EarnRewardsPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, chai_1.expect)((0, helpers_2.deployProxy)("DBAPool", [ { admin: owner.address, x2EarnApps: await x2EarnApps.getAddress(), xAllocationPool: await xAllocationPool.getAddress(), x2earnRewardsPool: await x2EarnRewardsPool.getAddress(), b3tr: await b3tr.getAddress(), distributionStartRound: 0, }, ])).to.be.revertedWith("DBAPool: distribution start round is zero"); }); }); (0, mocha_1.describe)("Contract Upgradeability", () => { (0, mocha_1.it)("Admin with UPGRADER_ROLE should be able to upgrade the contract", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); // Deploy the implementation contract const Contract = await hardhat_1.ethers.getContractFactory("DBAPool"); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); const currentImplAddress = await (0, upgrades_core_1.getImplementationAddress)(hardhat_1.ethers.provider, await dynamicBaseAllocationPool.getAddress()); const UPGRADER_ROLE = await dynamicBaseAllocationPool.UPGRADER_ROLE(); // Grant upgrader role to owner await dynamicBaseAllocationPool.connect(owner).grantRole(UPGRADER_ROLE, owner.address); (0, chai_1.expect)(await dynamicBaseAllocationPool.hasRole(UPGRADER_ROLE, owner.address)).to.eql(true); await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).upgradeToAndCall(await implementation.getAddress(), "0x")) .to.not.be.reverted; const newImplAddress = await (0, upgrades_core_1.getImplementationAddress)(hardhat_1.ethers.provider, await dynamicBaseAllocationPool.getAddress()); (0, chai_1.expect)(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase()); (0, chai_1.expect)(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase()); }); (0, mocha_1.it)("Only accounts with UPGRADER_ROLE should be able to upgrade the contract", async function () { const { dynamicBaseAllocationPool, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); // Deploy the implementation contract const Contract = await hardhat_1.ethers.getContractFactory("DBAPool"); const implementation = await Contract.deploy(); await implementation.waitForDeployment(); const UPGRADER_ROLE = await dynamicBaseAllocationPool.UPGRADER_ROLE(); await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x")); }); }); (0, mocha_1.describe)("Role Management", () => { (0, mocha_1.it)("Should assign DEFAULT_ADMIN_ROLE to admin on initialization", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DEFAULT_ADMIN_ROLE = await dynamicBaseAllocationPool.DEFAULT_ADMIN_ROLE(); (0, chai_1.expect)(await dynamicBaseAllocationPool.hasRole(DEFAULT_ADMIN_ROLE, owner.address)).to.eql(true); }); (0, mocha_1.it)("Admin should be able to grant DISTRIBUTOR_ROLE", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address); (0, chai_1.expect)(await dynamicBaseAllocationPool.hasRole(DISTRIBUTOR_ROLE, distributor.address)).to.eql(true); }); (0, mocha_1.it)("Admin should be able to revoke DISTRIBUTOR_ROLE", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address); (0, chai_1.expect)(await dynamicBaseAllocationPool.hasRole(DISTRIBUTOR_ROLE, distributor.address)).to.eql(true); await dynamicBaseAllocationPool.connect(owner).revokeRole(DISTRIBUTOR_ROLE, distributor.address); (0, chai_1.expect)(await dynamicBaseAllocationPool.hasRole(DISTRIBUTOR_ROLE, distributor.address)).to.eql(false); }); (0, mocha_1.it)("Non-admin should not be able to grant roles", async function () { const { dynamicBaseAllocationPool, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).grantRole(DISTRIBUTOR_ROLE, otherAccount.address)); }); }); (0, mocha_1.describe)("Getter Functions", () => { (0, mocha_1.it)("Should return correct b3trBalance", async function () { const { dynamicBaseAllocationPool, b3tr, minterAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const amount = hardhat_1.ethers.parseEther("1000"); await b3tr.connect(minterAccount).mint(await dynamicBaseAllocationPool.getAddress(), amount); (0, chai_1.expect)(await dynamicBaseAllocationPool.b3trBalance()).to.eql(amount); }); (0, mocha_1.it)("Should return correct x2EarnApps address", async function () { const { dynamicBaseAllocationPool, x2EarnApps } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); (0, chai_1.expect)(await dynamicBaseAllocationPool.x2EarnApps()).to.eql(await x2EarnApps.getAddress()); }); (0, mocha_1.it)("Should return correct xAllocationPool address", async function () { const { dynamicBaseAllocationPool, xAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); (0, chai_1.expect)(await dynamicBaseAllocationPool.xAllocationPool()).to.eql(await xAllocationPool.getAddress()); }); (0, mocha_1.it)("Should return correct b3tr address", async function () { const { dynamicBaseAllocationPool, b3tr } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); (0, chai_1.expect)(await dynamicBaseAllocationPool.b3tr()).to.eql(await b3tr.getAddress()); }); (0, mocha_1.it)("Should return correct x2EarnRewardsPool address", async function () { const { dynamicBaseAllocationPool, x2EarnRewardsPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); (0, chai_1.expect)(await dynamicBaseAllocationPool.x2EarnRewardsPool()).to.eql(await x2EarnRewardsPool.getAddress()); }); (0, mocha_1.it)("Should return correct distributionStartRound", async function () { const { dynamicBaseAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); (0, chai_1.expect)(await dynamicBaseAllocationPool.distributionStartRound()).to.eql(1n); }); (0, mocha_1.it)("isDBARewardsDistributed should return false for new round", async function () { const { dynamicBaseAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); (0, chai_1.expect)(await dynamicBaseAllocationPool.isDBARewardsDistributed(1)).to.eql(false); }); (0, mocha_1.it)("fundsForRound should return unallocated funds from xAllocationPool", async function () { const { dynamicBaseAllocationPool, xAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const roundId = 1; const unallocatedFunds = await xAllocationPool.unallocatedFunds(roundId); (0, chai_1.expect)(await dynamicBaseAllocationPool.fundsForRound(roundId)).to.eql(unallocatedFunds); }); }); (0, mocha_1.describe)("Admin Functions", () => { (0, mocha_1.describe)("Pause/Unpause", () => { (0, mocha_1.it)("Admin should be able to pause the contract", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await dynamicBaseAllocationPool.connect(owner).pause(); (0, chai_1.expect)(await dynamicBaseAllocationPool.paused()).to.eql(true); }); (0, mocha_1.it)("Admin should be able to unpause the contract", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await dynamicBaseAllocationPool.connect(owner).pause(); (0, chai_1.expect)(await dynamicBaseAllocationPool.paused()).to.eql(true); await dynamicBaseAllocationPool.connect(owner).unpause(); (0, chai_1.expect)(await dynamicBaseAllocationPool.paused()).to.eql(false); }); (0, mocha_1.it)("Non-admin should not be able to pause the contract", async function () { const { dynamicBaseAllocationPool, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).pause()); }); (0, mocha_1.it)("Non-admin should not be able to unpause the contract", async function () { const { dynamicBaseAllocationPool, owner, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await dynamicBaseAllocationPool.connect(owner).pause(); await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).unpause()); }); (0, mocha_1.it)("Should not allow distribution when paused", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address); await dynamicBaseAllocationPool.connect(owner).pause(); const appIds = [hardhat_1.ethers.encodeBytes32String("app1")]; await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(distributor).distributeDBARewards(1, appIds)); }); }); (0, mocha_1.describe)("setX2EarnApps", () => { (0, mocha_1.it)("Admin should be able to update x2EarnApps", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const newAddress = hardhat_1.ethers.Wallet.createRandom().address; await dynamicBaseAllocationPool.connect(owner).setX2EarnApps(newAddress); (0, chai_1.expect)(await dynamicBaseAllocationPool.x2EarnApps()).to.eql(newAddress); }); (0, mocha_1.it)("Non-admin should not be able to update x2EarnApps", async function () { const { dynamicBaseAllocationPool, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const newAddress = hardhat_1.ethers.Wallet.createRandom().address; await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).setX2EarnApps(newAddress)); }); (0, mocha_1.it)("Should revert if x2EarnApps is set to zero address", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).setX2EarnApps(helpers_1.ZERO_ADDRESS)).to.be.revertedWith("DBAPool: zero address"); }); }); (0, mocha_1.describe)("setXAllocationPool", () => { (0, mocha_1.it)("Admin should be able to update xAllocationPool", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const newAddress = hardhat_1.ethers.Wallet.createRandom().address; await dynamicBaseAllocationPool.connect(owner).setXAllocationPool(newAddress); (0, chai_1.expect)(await dynamicBaseAllocationPool.xAllocationPool()).to.eql(newAddress); }); (0, mocha_1.it)("Non-admin should not be able to update xAllocationPool", async function () { const { dynamicBaseAllocationPool, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const newAddress = hardhat_1.ethers.Wallet.createRandom().address; await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).setXAllocationPool(newAddress)); }); (0, mocha_1.it)("Should revert if xAllocationPool is set to zero address", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).setXAllocationPool(helpers_1.ZERO_ADDRESS)).to.be.revertedWith("DBAPool: zero address"); }); }); (0, mocha_1.describe)("setX2EarnRewardsPool", () => { (0, mocha_1.it)("Admin should be able to update x2EarnRewardsPool", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const newAddress = hardhat_1.ethers.Wallet.createRandom().address; await dynamicBaseAllocationPool.connect(owner).setX2EarnRewardsPool(newAddress); (0, chai_1.expect)(await dynamicBaseAllocationPool.x2EarnRewardsPool()).to.eql(newAddress); }); (0, mocha_1.it)("Non-admin should not be able to update x2EarnRewardsPool", async function () { const { dynamicBaseAllocationPool, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const newAddress = hardhat_1.ethers.Wallet.createRandom().address; await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).setX2EarnRewardsPool(newAddress)); }); (0, mocha_1.it)("Should revert if x2EarnRewardsPool is set to zero address", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).setX2EarnRewardsPool(helpers_1.ZERO_ADDRESS)).to.be.revertedWith("DBAPool: zero address"); }); }); (0, mocha_1.describe)("setDistributionStartRound", () => { (0, mocha_1.it)("Admin should be able to update distributionStartRound", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const newStartRound = 10; await dynamicBaseAllocationPool.connect(owner).setDistributionStartRound(newStartRound); (0, chai_1.expect)(await dynamicBaseAllocationPool.distributionStartRound()).to.eql(BigInt(newStartRound)); }); (0, mocha_1.it)("Non-admin should not be able to update distributionStartRound", async function () { const { dynamicBaseAllocationPool, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const newStartRound = 10; await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).setDistributionStartRound(newStartRound)); }); (0, mocha_1.it)("Should revert if distributionStartRound is set to zero", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).setDistributionStartRound(0)).to.be.revertedWith("DBAPool: distribution start round is zero"); }); }); }); (0, mocha_1.describe)("canDistributeDBARewards", () => { (0, mocha_1.it)("Should return false if round is before distributionStartRound", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); await dynamicBaseAllocationPool.connect(owner).setDistributionStartRound(10); (0, chai_1.expect)(await dynamicBaseAllocationPool.canDistributeDBARewards(5)).to.eql(false); }); (0, mocha_1.it)("Should return false if rewards already distributed for round", async function () { const { dynamicBaseAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); // This would require actually distributing rewards first, which we'll test in distribution tests (0, chai_1.expect)(await dynamicBaseAllocationPool.canDistributeDBARewards(1)).to.be.a("boolean"); }); (0, mocha_1.it)("Should return false if no unallocated funds for round", async function () { const { dynamicBaseAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); // For a brand new round with no activity (0, chai_1.expect)(await dynamicBaseAllocationPool.canDistributeDBARewards(999999)).to.eql(false); }); (0, mocha_1.it)("Should return false if not all funds claimed for round", async function () { const { dynamicBaseAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); // This tests the allFundsClaimed requirement const result = await dynamicBaseAllocationPool.canDistributeDBARewards(1); (0, chai_1.expect)(result).to.be.a("boolean"); }); }); (0, mocha_1.describe)("DBA Rewards Distribution", () => { (0, mocha_1.it)("Should revert if caller doesn't have DISTRIBUTOR_ROLE", async function () { const { dynamicBaseAllocationPool, otherAccount } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const appIds = [hardhat_1.ethers.encodeBytes32String("app1")]; await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(otherAccount).distributeDBARewards(1, appIds)); }); (0, mocha_1.it)("Should revert if no apps provided", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address); await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(distributor).distributeDBARewards(1, [])).to.be.revertedWith("DBAPool: no apps to distribute to"); }); (0, mocha_1.it)("Should revert if round is before distributionStartRound", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address); await dynamicBaseAllocationPool.connect(owner).setDistributionStartRound(10); const appIds = [hardhat_1.ethers.encodeBytes32String("app1")]; await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(distributor).distributeDBARewards(5, appIds)).to.be.revertedWith("DBAPool: Round invalid or not ready to distribute"); }); (0, mocha_1.it)("Should revert if trying to distribute twice for same round", async function () { const { dynamicBaseAllocationPool, owner, x2EarnApps, b3tr } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, owner.address); // This test will need proper setup with a completed round // For now, we verify the check exists (0, chai_1.expect)(await dynamicBaseAllocationPool.isDBARewardsDistributed(1)).to.eql(false); }); (0, mocha_1.it)("Should revert if contract has no B3TR balance", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address); const appIds = [hardhat_1.ethers.encodeBytes32String("app1")]; // This will fail on one of the validation checks await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(distributor).distributeDBARewards(1, appIds)); }); (0, mocha_1.it)("Should revert if app does not exist", async function () { const { dynamicBaseAllocationPool, owner, b3tr, minterAccount, xAllocationPool } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, owner.address); // Give DBA pool some funds await b3tr.connect(minterAccount).mint(await dynamicBaseAllocationPool.getAddress(), hardhat_1.ethers.parseEther("1000")); // Use a non-existent app ID const nonExistentAppId = hardhat_1.ethers.encodeBytes32String("nonexistent"); // This will fail validation checks before getting to app existence check await (0, helpers_1.catchRevert)(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, [nonExistentAppId])); }); (0, mocha_1.it)("Should revert if duplicate app IDs are provided", async function () { const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, owner.address); // Create random app IDs const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app1")); const app2Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app2")); const app3Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app3")); // Try to distribute with duplicate app IDs - should revert before any other checks await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, [app1Id, app2Id, app1Id])).to.be.revertedWith("DBAPool: duplicate app IDs not allowed"); // Verify with consecutive duplicates await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, [app2Id, app2Id])).to.be.revertedWith("DBAPool: duplicate app IDs not allowed"); // Verify with duplicates at the end await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, [app1Id, app2Id, app3Id, app2Id])).to.be.revertedWith("DBAPool: duplicate app IDs not allowed"); }); (0, mocha_1.it)("Should handle large number of app IDs efficiently (scalability test)", async function () { this.timeout(180000); // 3 minutes timeout for this test const { dynamicBaseAllocationPool, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, owner.address); // Create 1000 unique app IDs const appIds = []; for (let i = 0; i < 1000; i++) { appIds.push(hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(`app${i}`))); } // Add a duplicate at the end to test worst-case scenario appIds.push(appIds[0]); // This should detect the duplicate even with 1001 items await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, appIds)).to.be.revertedWith("DBAPool: duplicate app IDs not allowed"); }); }); (0, mocha_1.describe)("Integration Tests - Full Distribution Flow", () => { (0, mocha_1.it)("Should successfully distribute rewards to a single eligible app", async function () { this.timeout(120000); const config = (0, local_1.createLocalConfig)(); config.EMISSIONS_CYCLE_DURATION = 10; config.INITIAL_X_ALLOCATION = hardhat_1.ethers.parseEther("10000"); config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 50; const { dynamicBaseAllocationPool, owner, x2EarnApps, xAllocationPool, xAllocationVoting, emissions, b3tr, minterAccount, otherAccounts, veBetterPassport, creators, x2EarnRewardsPool, } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, helpers_1.bootstrapEmissions)(); await veBetterPassport.whitelist(otherAccounts[0].address); await veBetterPassport.whitelist(otherAccounts[1].address); await veBetterPassport.toggleCheck(1); await (0, helpers_1.getVot3Tokens)(otherAccounts[0], "10000"); await (0, helpers_1.getVot3Tokens)(otherAccounts[1], "10000"); // Create two apps const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app1")); const app2Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app2")); await x2EarnApps .connect(creators[0]) .submitApp(otherAccounts[5].address, otherAccounts[5].address, "app1", "metadataURI"); await x2EarnApps .connect(creators[1]) .submitApp(otherAccounts[6].address, otherAccounts[6].address, "app2", "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, otherAccounts[0]); await (0, xnodes_1.endorseApp)(app2Id, otherAccounts[1]); await emissions.connect(minterAccount).start(); const round1 = await xAllocationVoting.currentRoundId(); // Vote heavily on app1 to exceed cap, lightly on app2 await xAllocationVoting.connect(otherAccounts[0]).castVote(round1, [app1Id], [hardhat_1.ethers.parseEther("9000")]); await xAllocationVoting.connect(otherAccounts[1]).castVote(round1, [app2Id], [hardhat_1.ethers.parseEther("1000")]); await (0, helpers_1.waitForRoundToEnd)(round1); await xAllocationPool .connect(owner) .setUnallocatedFundsReceiverAddress(await dynamicBaseAllocationPool.getAddress()); await xAllocationPool.claim(round1, app1Id); await xAllocationPool.claim(round1, app2Id); const unallocatedAmount = await xAllocationPool.unallocatedFunds(round1); (0, chai_1.expect)(unallocatedAmount).to.equal(hardhat_1.ethers.parseEther("2800")); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, owner.address); const initialApp2Funds = await x2EarnRewardsPool.availableFunds(app2Id); // Compute expected reward: min(flatShare, meritCap) // Merit cap now uses vote-only allocation (totalEarnings - baseAllocation) const [app2TotalEarnings] = await xAllocationPool.roundEarnings(round1, app2Id); const baseAllocation = await xAllocationPool.baseAllocationAmount(round1); const app2VoteOnly = app2TotalEarnings - baseAllocation; const meritCap = app2VoteOnly * 2n; const flatShare = unallocatedAmount; // single app gets all const expectedReward = flatShare < meritCap ? flatShare : meritCap; // Distribute to single app const tx = await dynamicBaseAllocationPool.connect(owner).distributeDBARewards(round1, [app2Id]); await (0, chai_1.expect)(tx) .to.emit(dynamicBaseAllocationPool, "FundsDistributedToApp") .withArgs(app2Id, expectedReward, round1); const finalApp2Funds = await x2EarnRewardsPool.availableFunds(app2Id); (0, chai_1.expect)(finalApp2Funds - initialApp2Funds).to.equal(expectedReward); (0, chai_1.expect)(await dynamicBaseAllocationPool.b3trBalance()).to.equal(0n); (0, chai_1.expect)(await dynamicBaseAllocationPool.isDBARewardsDistributed(round1)).to.equal(true); }); (0, mocha_1.it)("Should successfully store distributed rewards for apps", async function () { this.timeout(120000); const config = (0, local_1.createLocalConfig)(); config.EMISSIONS_CYCLE_DURATION = 10; config.INITIAL_X_ALLOCATION = hardhat_1.ethers.parseEther("10000"); config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 50; const { dynamicBaseAllocationPool, owner, x2EarnApps, xAllocationPool, xAllocationVoting, emissions, minterAccount, otherAccounts, veBetterPassport, creators, } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, helpers_1.bootstrapEmissions)(); await veBetterPassport.whitelist(otherAccounts[0].address); await veBetterPassport.whitelist(otherAccounts[1].address); await veBetterPassport.toggleCheck(1); await (0, helpers_1.getVot3Tokens)(otherAccounts[0], "10000"); await (0, helpers_1.getVot3Tokens)(otherAccounts[1], "10000"); // Create two apps const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app1")); const app2Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app2")); await x2EarnApps .connect(creators[0]) .submitApp(otherAccounts[5].address, otherAccounts[5].address, "app1", "metadataURI"); await x2EarnApps .connect(creators[1]) .submitApp(otherAccounts[6].address, otherAccounts[6].address, "app2", "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, otherAccounts[0]); await (0, xnodes_1.endorseApp)(app2Id, otherAccounts[1]); await emissions.connect(minterAccount).start(); const round1 = await xAllocationVoting.currentRoundId(); // Vote heavily on app1 to exceed cap, lightly on app2 await xAllocationVoting.connect(otherAccounts[0]).castVote(round1, [app1Id], [hardhat_1.ethers.parseEther("9000")]); await xAllocationVoting.connect(otherAccounts[1]).castVote(round1, [app2Id], [hardhat_1.ethers.parseEther("1000")]); await (0, helpers_1.waitForRoundToEnd)(round1); await xAllocationPool .connect(owner) .setUnallocatedFundsReceiverAddress(await dynamicBaseAllocationPool.getAddress()); await xAllocationPool.claim(round1, app1Id); await xAllocationPool.claim(round1, app2Id); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, owner.address); // Compute expected reward for round 1 const unallocatedR1 = await xAllocationPool.unallocatedFunds(round1); const [app2EarningsR1] = await xAllocationPool.roundEarnings(round1, app2Id); const baseAllocR1 = await xAllocationPool.baseAllocationAmount(round1); const meritCapR1 = (app2EarningsR1 - baseAllocR1) * 2n; const expectedR1 = unallocatedR1 < meritCapR1 ? unallocatedR1 : meritCapR1; await dynamicBaseAllocationPool.connect(owner).distributeDBARewards(round1, [app2Id]); const dbaRoundRewardsForApp1Round1 = await dynamicBaseAllocationPool.dbaRoundRewardsForApp(round1, app1Id); (0, chai_1.expect)(dbaRoundRewardsForApp1Round1).to.equal(0n); const dbaRoundRewardsForApp2Round1 = await dynamicBaseAllocationPool.dbaRoundRewardsForApp(round1, app2Id); (0, chai_1.expect)(dbaRoundRewardsForApp2Round1).to.equal(expectedR1); const round2 = await (0, helpers_1.startNewAllocationRound)(); await xAllocationVoting.connect(otherAccounts[0]).castVote(round2, [app1Id], [hardhat_1.ethers.parseEther("9000")]); await xAllocationVoting.connect(otherAccounts[1]).castVote(round2, [app2Id], [hardhat_1.ethers.parseEther("1000")]); await (0, helpers_1.waitForRoundToEnd)(round2); await xAllocationPool.claim(round2, app1Id); await xAllocationPool.claim(round2, app2Id); // Compute expected reward for round 2 const unallocatedR2 = await xAllocationPool.unallocatedFunds(round2); const [app2EarningsR2] = await xAllocationPool.roundEarnings(round2, app2Id); const baseAllocR2 = await xAllocationPool.baseAllocationAmount(round2); const meritCapR2 = (app2EarningsR2 - baseAllocR2) * 2n; const expectedR2 = unallocatedR2 < meritCapR2 ? unallocatedR2 : meritCapR2; await dynamicBaseAllocationPool.connect(owner).distributeDBARewards(round2, [app2Id]); const dbaRoundRewardsForApp1Round2 = await dynamicBaseAllocationPool.dbaRoundRewardsForApp(round2, app1Id); (0, chai_1.expect)(dbaRoundRewardsForApp1Round2).to.equal(0n); const dbaRoundRewardsForApp2Round2 = await dynamicBaseAllocationPool.dbaRoundRewardsForApp(round2, app2Id); (0, chai_1.expect)(dbaRoundRewardsForApp2Round2).to.equal(expectedR2); }); (0, mocha_1.it)("Should revert when trying to distribute to non-existent app", async function () { this.timeout(120000); const config = (0, local_1.createLocalConfig)(); config.EMISSIONS_CYCLE_DURATION = 10; config.INITIAL_X_ALLOCATION = hardhat_1.ethers.parseEther("10000"); config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 50; const { dynamicBaseAllocationPool, owner, x2EarnApps, xAllocationPool, xAllocationVoting, emissions, b3tr, minterAccount, otherAccounts, veBetterPassport, creators, x2EarnRewardsPool, } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, helpers_1.bootstrapEmissions)(); await veBetterPassport.whitelist(otherAccounts[0].address); await veBetterPassport.toggleCheck(1); await (0, helpers_1.getVot3Tokens)(otherAccounts[0], "10000"); const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app1")); const nonExistentAppId = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("nonExistentApp")); await x2EarnApps .connect(creators[0]) .submitApp(otherAccounts[5].address, otherAccounts[5].address, "app1", "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, otherAccounts[0]); await emissions.connect(minterAccount).start(); const round1 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(otherAccounts[0]).castVote(round1, [app1Id], [hardhat_1.ethers.parseEther("8000")]); await (0, helpers_1.waitForRoundToEnd)(round1); await xAllocationPool .connect(owner) .setUnallocatedFundsReceiverAddress(await dynamicBaseAllocationPool.getAddress()); await xAllocationPool.claim(round1, app1Id); const unallocatedAmount = await xAllocationPool.unallocatedFunds(round1); (0, chai_1.expect)(unallocatedAmount).to.equal(hardhat_1.ethers.parseEther("3500")); const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE(); await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, owner.address); // Try to distribute to non-existent app await (0, chai_1.expect)(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(round1, [nonExistentAppId])).to.be.revertedWith("DBAPool: app does not exist"); // Verify can still distribute to existing app after failed attempt const initialApp1Funds = await x2EarnRewardsPool.availableFunds(app1Id); await dynamicBaseAllocationPool.connect(owner).distributeDBARewards(round1, [app1Id]); const finalApp1Funds = await x2EarnRewardsPool.availableFunds(app1Id); (0, chai_1.expect)(finalApp1Funds - initialApp1Funds).to.equal(hardhat_1.ethers.parseEther("3500")); }); (0, mocha_1.it)("Should not allow distribution before all apps have claimed their rewards", async function () { this.timeout(120000); const config = (0, local_1.createLocalConfig)(); config.EMISSIONS_CYCLE_DURATION = 10; config.INITIAL_X_ALLOCATION = hardhat_1.ethers.parseEther("10000"); config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 50; const { dynamicBaseAllocationPool, owner, x2EarnApps, xAllocationPool, xAllocationVoting, emissions, b3tr, minterAccount, otherAccounts, veBetterPassport, creators, x2EarnRewardsPool, } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, config, }); await (0, helpers_1.bootstrapEmissions)(); await veBetterPassport.whitelist(otherAccounts[0].address); await veBetterPassport.whitelist(otherAccounts[1].address); await veBetterPassport.toggleCheck(1); await (0, helpers_1.getVot3Tokens)(otherAccounts[0], "10000"); await (0, helpers_1.getVot3Tokens)(otherAccounts[1], "10000"); const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app1")); const app2Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("app2")); await x2EarnApps .connect(creators[0]) .submitApp(otherAccounts[5].address, otherAccounts[5].address, "app1", "metadataURI"); await x2EarnApps .connect(creators[1]) .submitApp(otherAccounts[6].address, otherAccounts[6].address, "app2", "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, otherAccounts[0]); await (0, xnodes_1.endorseApp)(app2Id, otherAccounts[1]); await emissions.connect(minterAccount).start(); const round1 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(otherAccounts[0]).castVote(round1, [app1Id], [hardhat_1.ethers.parseEther("8000")]); await xAllocationVoting.connect(otherAccounts[1]).castVote(round1, [app2Id], [hardhat_1.ethers.parseEther("2000")]); await (0, helpers_1.waitForRoundToEnd)(round1); await xAllocationPool .connect(owner) .setUnallocatedFundsReceiverAddress(await dynamicBaseAllocationPool.getAddress()); // Only app1 claims, app2 does not await xAllocationPool.claim(round1, app1Id); const allFundsClaimed =