@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
JavaScript
"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 =