@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
786 lines • 93 kB
JavaScript
import { ethers } from "hardhat";
import { expect } from "chai";
import { ZERO_ADDRESS, bootstrapEmissions, catchRevert, getOrDeployContractInstances, getVot3Tokens, waitForRoundToEnd, startNewAllocationRound, } from "./helpers";
import { describe, it, before } from "mocha";
import { getImplementationAddress } from "@openzeppelin/upgrades-core";
import { createLocalConfig } from "@repo/config/contracts/envs/local";
import { deployProxy } from "../scripts/helpers";
import { endorseApp } from "./helpers/xnodes";
describe("DBA Pool - @shard7b", async function () {
// Environment params
let owner;
let otherAccount;
let distributor;
let upgrader;
before(async function () {
const { owner: deployedOwner, otherAccount: deployedOtherAccount, otherAccounts, } = await getOrDeployContractInstances({ forceDeploy: true });
owner = deployedOwner;
otherAccount = deployedOtherAccount;
distributor = otherAccounts[0];
upgrader = otherAccounts[1];
});
describe("Deployment and Initialization", async function () {
it("Contract is correctly initialized", async function () {
const { dynamicBaseAllocationPool, x2EarnApps, xAllocationPool, x2EarnRewardsPool, b3tr } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await dynamicBaseAllocationPool.x2EarnApps()).to.eql(await x2EarnApps.getAddress());
expect(await dynamicBaseAllocationPool.xAllocationPool()).to.eql(await xAllocationPool.getAddress());
expect(await dynamicBaseAllocationPool.x2EarnRewardsPool()).to.eql(await x2EarnRewardsPool.getAddress());
expect(await dynamicBaseAllocationPool.b3tr()).to.eql(await b3tr.getAddress());
expect(await dynamicBaseAllocationPool.distributionStartRound()).to.eql(1n);
const DEFAULT_ADMIN_ROLE = await dynamicBaseAllocationPool.DEFAULT_ADMIN_ROLE();
expect(await dynamicBaseAllocationPool.hasRole(DEFAULT_ADMIN_ROLE, owner.address)).to.eql(true);
});
it("Should return correct version", async function () {
const { dynamicBaseAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await dynamicBaseAllocationPool.version()).to.eql("3");
});
it("Should revert if admin is set to zero address in initialization", async () => {
const config = createLocalConfig();
const { b3tr, x2EarnApps, xAllocationPool, x2EarnRewardsPool } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
await expect(deployProxy("DBAPool", [
{
admin: 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");
});
it("Should revert if x2EarnApps is set to zero address in initialization", async () => {
const config = createLocalConfig();
const { owner, b3tr, xAllocationPool, x2EarnRewardsPool } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
await expect(deployProxy("DBAPool", [
{
admin: owner.address,
x2EarnApps: 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");
});
it("Should revert if xAllocationPool is set to zero address in initialization", async () => {
const config = createLocalConfig();
const { owner, b3tr, x2EarnApps, x2EarnRewardsPool } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
await expect(deployProxy("DBAPool", [
{
admin: owner.address,
x2EarnApps: await x2EarnApps.getAddress(),
xAllocationPool: ZERO_ADDRESS,
x2earnRewardsPool: await x2EarnRewardsPool.getAddress(),
b3tr: await b3tr.getAddress(),
distributionStartRound: 1,
},
])).to.be.revertedWith("DBAPool: xAllocationPool is the zero address");
});
it("Should revert if x2earnRewardsPool is set to zero address in initialization", async () => {
const config = createLocalConfig();
const { owner, b3tr, x2EarnApps, xAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
await expect(deployProxy("DBAPool", [
{
admin: owner.address,
x2EarnApps: await x2EarnApps.getAddress(),
xAllocationPool: await xAllocationPool.getAddress(),
x2earnRewardsPool: ZERO_ADDRESS,
b3tr: await b3tr.getAddress(),
distributionStartRound: 1,
},
])).to.be.revertedWith("DBAPool: x2EarnRewardsPool is the zero address");
});
it("Should revert if b3tr is set to zero address in initialization", async () => {
const config = createLocalConfig();
const { owner, x2EarnApps, xAllocationPool, x2EarnRewardsPool } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
await expect(deployProxy("DBAPool", [
{
admin: owner.address,
x2EarnApps: await x2EarnApps.getAddress(),
xAllocationPool: await xAllocationPool.getAddress(),
x2earnRewardsPool: await x2EarnRewardsPool.getAddress(),
b3tr: ZERO_ADDRESS,
distributionStartRound: 1,
},
])).to.be.revertedWith("DBAPool: b3tr is the zero address");
});
it("Should revert if distributionStartRound is zero in initialization", async () => {
const config = createLocalConfig();
const { owner, b3tr, x2EarnApps, xAllocationPool, x2EarnRewardsPool } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
await expect(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");
});
});
describe("Contract Upgradeability", () => {
it("Admin with UPGRADER_ROLE should be able to upgrade the contract", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
// Deploy the implementation contract
const Contract = await ethers.getContractFactory("DBAPool");
const implementation = await Contract.deploy();
await implementation.waitForDeployment();
const currentImplAddress = await getImplementationAddress(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);
expect(await dynamicBaseAllocationPool.hasRole(UPGRADER_ROLE, owner.address)).to.eql(true);
await expect(dynamicBaseAllocationPool.connect(owner).upgradeToAndCall(await implementation.getAddress(), "0x"))
.to.not.be.reverted;
const newImplAddress = await getImplementationAddress(ethers.provider, await dynamicBaseAllocationPool.getAddress());
expect(newImplAddress.toUpperCase()).to.not.eql(currentImplAddress.toUpperCase());
expect(newImplAddress.toUpperCase()).to.eql((await implementation.getAddress()).toUpperCase());
});
it("Only accounts with UPGRADER_ROLE should be able to upgrade the contract", async function () {
const { dynamicBaseAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
// Deploy the implementation contract
const Contract = await ethers.getContractFactory("DBAPool");
const implementation = await Contract.deploy();
await implementation.waitForDeployment();
const UPGRADER_ROLE = await dynamicBaseAllocationPool.UPGRADER_ROLE();
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).upgradeToAndCall(await implementation.getAddress(), "0x"));
});
});
describe("Role Management", () => {
it("Should assign DEFAULT_ADMIN_ROLE to admin on initialization", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const DEFAULT_ADMIN_ROLE = await dynamicBaseAllocationPool.DEFAULT_ADMIN_ROLE();
expect(await dynamicBaseAllocationPool.hasRole(DEFAULT_ADMIN_ROLE, owner.address)).to.eql(true);
});
it("Admin should be able to grant DISTRIBUTOR_ROLE", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE();
await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address);
expect(await dynamicBaseAllocationPool.hasRole(DISTRIBUTOR_ROLE, distributor.address)).to.eql(true);
});
it("Admin should be able to revoke DISTRIBUTOR_ROLE", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE();
await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address);
expect(await dynamicBaseAllocationPool.hasRole(DISTRIBUTOR_ROLE, distributor.address)).to.eql(true);
await dynamicBaseAllocationPool.connect(owner).revokeRole(DISTRIBUTOR_ROLE, distributor.address);
expect(await dynamicBaseAllocationPool.hasRole(DISTRIBUTOR_ROLE, distributor.address)).to.eql(false);
});
it("Non-admin should not be able to grant roles", async function () {
const { dynamicBaseAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE();
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).grantRole(DISTRIBUTOR_ROLE, otherAccount.address));
});
});
describe("Getter Functions", () => {
it("Should return correct b3trBalance", async function () {
const { dynamicBaseAllocationPool, b3tr, minterAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
const amount = ethers.parseEther("1000");
await b3tr.connect(minterAccount).mint(await dynamicBaseAllocationPool.getAddress(), amount);
expect(await dynamicBaseAllocationPool.b3trBalance()).to.eql(amount);
});
it("Should return correct x2EarnApps address", async function () {
const { dynamicBaseAllocationPool, x2EarnApps } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await dynamicBaseAllocationPool.x2EarnApps()).to.eql(await x2EarnApps.getAddress());
});
it("Should return correct xAllocationPool address", async function () {
const { dynamicBaseAllocationPool, xAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await dynamicBaseAllocationPool.xAllocationPool()).to.eql(await xAllocationPool.getAddress());
});
it("Should return correct b3tr address", async function () {
const { dynamicBaseAllocationPool, b3tr } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await dynamicBaseAllocationPool.b3tr()).to.eql(await b3tr.getAddress());
});
it("Should return correct x2EarnRewardsPool address", async function () {
const { dynamicBaseAllocationPool, x2EarnRewardsPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await dynamicBaseAllocationPool.x2EarnRewardsPool()).to.eql(await x2EarnRewardsPool.getAddress());
});
it("Should return correct distributionStartRound", async function () {
const { dynamicBaseAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await dynamicBaseAllocationPool.distributionStartRound()).to.eql(1n);
});
it("isDBARewardsDistributed should return false for new round", async function () {
const { dynamicBaseAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
expect(await dynamicBaseAllocationPool.isDBARewardsDistributed(1)).to.eql(false);
});
it("fundsForRound should return unallocated funds from xAllocationPool", async function () {
const { dynamicBaseAllocationPool, xAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
const roundId = 1;
const unallocatedFunds = await xAllocationPool.unallocatedFunds(roundId);
expect(await dynamicBaseAllocationPool.fundsForRound(roundId)).to.eql(unallocatedFunds);
});
});
describe("Admin Functions", () => {
describe("Pause/Unpause", () => {
it("Admin should be able to pause the contract", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
await dynamicBaseAllocationPool.connect(owner).pause();
expect(await dynamicBaseAllocationPool.paused()).to.eql(true);
});
it("Admin should be able to unpause the contract", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
await dynamicBaseAllocationPool.connect(owner).pause();
expect(await dynamicBaseAllocationPool.paused()).to.eql(true);
await dynamicBaseAllocationPool.connect(owner).unpause();
expect(await dynamicBaseAllocationPool.paused()).to.eql(false);
});
it("Non-admin should not be able to pause the contract", async function () {
const { dynamicBaseAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).pause());
});
it("Non-admin should not be able to unpause the contract", async function () {
const { dynamicBaseAllocationPool, owner, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
await dynamicBaseAllocationPool.connect(owner).pause();
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).unpause());
});
it("Should not allow distribution when paused", async function () {
const { dynamicBaseAllocationPool, owner } = await 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 = [ethers.encodeBytes32String("app1")];
await catchRevert(dynamicBaseAllocationPool.connect(distributor).distributeDBARewards(1, appIds));
});
});
describe("setX2EarnApps", () => {
it("Admin should be able to update x2EarnApps", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newAddress = ethers.Wallet.createRandom().address;
await dynamicBaseAllocationPool.connect(owner).setX2EarnApps(newAddress);
expect(await dynamicBaseAllocationPool.x2EarnApps()).to.eql(newAddress);
});
it("Non-admin should not be able to update x2EarnApps", async function () {
const { dynamicBaseAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newAddress = ethers.Wallet.createRandom().address;
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).setX2EarnApps(newAddress));
});
it("Should revert if x2EarnApps is set to zero address", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
await expect(dynamicBaseAllocationPool.connect(owner).setX2EarnApps(ZERO_ADDRESS)).to.be.revertedWith("DBAPool: zero address");
});
});
describe("setXAllocationPool", () => {
it("Admin should be able to update xAllocationPool", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newAddress = ethers.Wallet.createRandom().address;
await dynamicBaseAllocationPool.connect(owner).setXAllocationPool(newAddress);
expect(await dynamicBaseAllocationPool.xAllocationPool()).to.eql(newAddress);
});
it("Non-admin should not be able to update xAllocationPool", async function () {
const { dynamicBaseAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newAddress = ethers.Wallet.createRandom().address;
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).setXAllocationPool(newAddress));
});
it("Should revert if xAllocationPool is set to zero address", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
await expect(dynamicBaseAllocationPool.connect(owner).setXAllocationPool(ZERO_ADDRESS)).to.be.revertedWith("DBAPool: zero address");
});
});
describe("setX2EarnRewardsPool", () => {
it("Admin should be able to update x2EarnRewardsPool", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newAddress = ethers.Wallet.createRandom().address;
await dynamicBaseAllocationPool.connect(owner).setX2EarnRewardsPool(newAddress);
expect(await dynamicBaseAllocationPool.x2EarnRewardsPool()).to.eql(newAddress);
});
it("Non-admin should not be able to update x2EarnRewardsPool", async function () {
const { dynamicBaseAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newAddress = ethers.Wallet.createRandom().address;
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).setX2EarnRewardsPool(newAddress));
});
it("Should revert if x2EarnRewardsPool is set to zero address", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
await expect(dynamicBaseAllocationPool.connect(owner).setX2EarnRewardsPool(ZERO_ADDRESS)).to.be.revertedWith("DBAPool: zero address");
});
});
describe("setDistributionStartRound", () => {
it("Admin should be able to update distributionStartRound", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newStartRound = 10;
await dynamicBaseAllocationPool.connect(owner).setDistributionStartRound(newStartRound);
expect(await dynamicBaseAllocationPool.distributionStartRound()).to.eql(BigInt(newStartRound));
});
it("Non-admin should not be able to update distributionStartRound", async function () {
const { dynamicBaseAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
const newStartRound = 10;
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).setDistributionStartRound(newStartRound));
});
it("Should revert if distributionStartRound is set to zero", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
await expect(dynamicBaseAllocationPool.connect(owner).setDistributionStartRound(0)).to.be.revertedWith("DBAPool: distribution start round is zero");
});
});
});
describe("canDistributeDBARewards", () => {
it("Should return false if round is before distributionStartRound", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
await dynamicBaseAllocationPool.connect(owner).setDistributionStartRound(10);
expect(await dynamicBaseAllocationPool.canDistributeDBARewards(5)).to.eql(false);
});
it("Should return false if rewards already distributed for round", async function () {
const { dynamicBaseAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
// This would require actually distributing rewards first, which we'll test in distribution tests
expect(await dynamicBaseAllocationPool.canDistributeDBARewards(1)).to.be.a("boolean");
});
it("Should return false if no unallocated funds for round", async function () {
const { dynamicBaseAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
// For a brand new round with no activity
expect(await dynamicBaseAllocationPool.canDistributeDBARewards(999999)).to.eql(false);
});
it("Should return false if not all funds claimed for round", async function () {
const { dynamicBaseAllocationPool } = await getOrDeployContractInstances({
forceDeploy: true,
});
// This tests the allFundsClaimed requirement
const result = await dynamicBaseAllocationPool.canDistributeDBARewards(1);
expect(result).to.be.a("boolean");
});
});
describe("DBA Rewards Distribution", () => {
it("Should revert if caller doesn't have DISTRIBUTOR_ROLE", async function () {
const { dynamicBaseAllocationPool, otherAccount } = await getOrDeployContractInstances({
forceDeploy: true,
});
const appIds = [ethers.encodeBytes32String("app1")];
await catchRevert(dynamicBaseAllocationPool.connect(otherAccount).distributeDBARewards(1, appIds));
});
it("Should revert if no apps provided", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE();
await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address);
await expect(dynamicBaseAllocationPool.connect(distributor).distributeDBARewards(1, [])).to.be.revertedWith("DBAPool: no apps to distribute to");
});
it("Should revert if round is before distributionStartRound", async function () {
const { dynamicBaseAllocationPool, owner } = await 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 = [ethers.encodeBytes32String("app1")];
await expect(dynamicBaseAllocationPool.connect(distributor).distributeDBARewards(5, appIds)).to.be.revertedWith("DBAPool: Round invalid or not ready to distribute");
});
it("Should revert if trying to distribute twice for same round", async function () {
const { dynamicBaseAllocationPool, owner, x2EarnApps, b3tr } = await 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
expect(await dynamicBaseAllocationPool.isDBARewardsDistributed(1)).to.eql(false);
});
it("Should revert if contract has no B3TR balance", async function () {
const { dynamicBaseAllocationPool, owner } = await getOrDeployContractInstances({
forceDeploy: true,
});
const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE();
await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, distributor.address);
const appIds = [ethers.encodeBytes32String("app1")];
// This will fail on one of the validation checks
await catchRevert(dynamicBaseAllocationPool.connect(distributor).distributeDBARewards(1, appIds));
});
it("Should revert if app does not exist", async function () {
const { dynamicBaseAllocationPool, owner, b3tr, minterAccount, xAllocationPool } = await 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(), ethers.parseEther("1000"));
// Use a non-existent app ID
const nonExistentAppId = ethers.encodeBytes32String("nonexistent");
// This will fail validation checks before getting to app existence check
await catchRevert(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, [nonExistentAppId]));
});
it("Should revert if duplicate app IDs are provided", async function () {
const { dynamicBaseAllocationPool, owner } = await 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 = ethers.keccak256(ethers.toUtf8Bytes("app1"));
const app2Id = ethers.keccak256(ethers.toUtf8Bytes("app2"));
const app3Id = ethers.keccak256(ethers.toUtf8Bytes("app3"));
// Try to distribute with duplicate app IDs - should revert before any other checks
await expect(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, [app1Id, app2Id, app1Id])).to.be.revertedWith("DBAPool: duplicate app IDs not allowed");
// Verify with consecutive duplicates
await expect(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, [app2Id, app2Id])).to.be.revertedWith("DBAPool: duplicate app IDs not allowed");
// Verify with duplicates at the end
await expect(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, [app1Id, app2Id, app3Id, app2Id])).to.be.revertedWith("DBAPool: duplicate app IDs not allowed");
});
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 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(ethers.keccak256(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 expect(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(1, appIds)).to.be.revertedWith("DBAPool: duplicate app IDs not allowed");
});
});
describe("Integration Tests - Full Distribution Flow", () => {
it("Should successfully distribute rewards to a single eligible app", async function () {
this.timeout(120000);
const config = createLocalConfig();
config.EMISSIONS_CYCLE_DURATION = 10;
config.INITIAL_X_ALLOCATION = 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 getOrDeployContractInstances({
forceDeploy: true,
config,
});
await bootstrapEmissions();
await veBetterPassport.whitelist(otherAccounts[0].address);
await veBetterPassport.whitelist(otherAccounts[1].address);
await veBetterPassport.toggleCheck(1);
await getVot3Tokens(otherAccounts[0], "10000");
await getVot3Tokens(otherAccounts[1], "10000");
// Create two apps
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("app1"));
const app2Id = ethers.keccak256(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 endorseApp(app1Id, otherAccounts[0]);
await 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], [ethers.parseEther("9000")]);
await xAllocationVoting.connect(otherAccounts[1]).castVote(round1, [app2Id], [ethers.parseEther("1000")]);
await 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);
expect(unallocatedAmount).to.equal(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 expect(tx)
.to.emit(dynamicBaseAllocationPool, "FundsDistributedToApp")
.withArgs(app2Id, expectedReward, round1);
const finalApp2Funds = await x2EarnRewardsPool.availableFunds(app2Id);
expect(finalApp2Funds - initialApp2Funds).to.equal(expectedReward);
expect(await dynamicBaseAllocationPool.b3trBalance()).to.equal(0n);
expect(await dynamicBaseAllocationPool.isDBARewardsDistributed(round1)).to.equal(true);
});
it("Should successfully store distributed rewards for apps", async function () {
this.timeout(120000);
const config = createLocalConfig();
config.EMISSIONS_CYCLE_DURATION = 10;
config.INITIAL_X_ALLOCATION = ethers.parseEther("10000");
config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 50;
const { dynamicBaseAllocationPool, owner, x2EarnApps, xAllocationPool, xAllocationVoting, emissions, minterAccount, otherAccounts, veBetterPassport, creators, } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
await bootstrapEmissions();
await veBetterPassport.whitelist(otherAccounts[0].address);
await veBetterPassport.whitelist(otherAccounts[1].address);
await veBetterPassport.toggleCheck(1);
await getVot3Tokens(otherAccounts[0], "10000");
await getVot3Tokens(otherAccounts[1], "10000");
// Create two apps
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("app1"));
const app2Id = ethers.keccak256(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 endorseApp(app1Id, otherAccounts[0]);
await 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], [ethers.parseEther("9000")]);
await xAllocationVoting.connect(otherAccounts[1]).castVote(round1, [app2Id], [ethers.parseEther("1000")]);
await 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);
expect(dbaRoundRewardsForApp1Round1).to.equal(0n);
const dbaRoundRewardsForApp2Round1 = await dynamicBaseAllocationPool.dbaRoundRewardsForApp(round1, app2Id);
expect(dbaRoundRewardsForApp2Round1).to.equal(expectedR1);
const round2 = await startNewAllocationRound();
await xAllocationVoting.connect(otherAccounts[0]).castVote(round2, [app1Id], [ethers.parseEther("9000")]);
await xAllocationVoting.connect(otherAccounts[1]).castVote(round2, [app2Id], [ethers.parseEther("1000")]);
await 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);
expect(dbaRoundRewardsForApp1Round2).to.equal(0n);
const dbaRoundRewardsForApp2Round2 = await dynamicBaseAllocationPool.dbaRoundRewardsForApp(round2, app2Id);
expect(dbaRoundRewardsForApp2Round2).to.equal(expectedR2);
});
it("Should revert when trying to distribute to non-existent app", async function () {
this.timeout(120000);
const config = createLocalConfig();
config.EMISSIONS_CYCLE_DURATION = 10;
config.INITIAL_X_ALLOCATION = 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 getOrDeployContractInstances({
forceDeploy: true,
config,
});
await bootstrapEmissions();
await veBetterPassport.whitelist(otherAccounts[0].address);
await veBetterPassport.toggleCheck(1);
await getVot3Tokens(otherAccounts[0], "10000");
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("app1"));
const nonExistentAppId = ethers.keccak256(ethers.toUtf8Bytes("nonExistentApp"));
await x2EarnApps
.connect(creators[0])
.submitApp(otherAccounts[5].address, otherAccounts[5].address, "app1", "metadataURI");
await endorseApp(app1Id, otherAccounts[0]);
await emissions.connect(minterAccount).start();
const round1 = await xAllocationVoting.currentRoundId();
await xAllocationVoting.connect(otherAccounts[0]).castVote(round1, [app1Id], [ethers.parseEther("8000")]);
await waitForRoundToEnd(round1);
await xAllocationPool
.connect(owner)
.setUnallocatedFundsReceiverAddress(await dynamicBaseAllocationPool.getAddress());
await xAllocationPool.claim(round1, app1Id);
const unallocatedAmount = await xAllocationPool.unallocatedFunds(round1);
expect(unallocatedAmount).to.equal(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 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);
expect(finalApp1Funds - initialApp1Funds).to.equal(ethers.parseEther("3500"));
});
it("Should not allow distribution before all apps have claimed their rewards", async function () {
this.timeout(120000);
const config = createLocalConfig();
config.EMISSIONS_CYCLE_DURATION = 10;
config.INITIAL_X_ALLOCATION = 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 getOrDeployContractInstances({
forceDeploy: true,
config,
});
await bootstrapEmissions();
await veBetterPassport.whitelist(otherAccounts[0].address);
await veBetterPassport.whitelist(otherAccounts[1].address);
await veBetterPassport.toggleCheck(1);
await getVot3Tokens(otherAccounts[0], "10000");
await getVot3Tokens(otherAccounts[1], "10000");
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("app1"));
const app2Id = ethers.keccak256(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 endorseApp(app1Id, otherAccounts[0]);
await endorseApp(app2Id, otherAccounts[1]);
await emissions.connect(minterAccount).start();
const round1 = await xAllocationVoting.currentRoundId();
await xAllocationVoting.connect(otherAccounts[0]).castVote(round1, [app1Id], [ethers.parseEther("8000")]);
await xAllocationVoting.connect(otherAccounts[1]).castVote(round1, [app2Id], [ethers.parseEther("2000")]);
await waitForRoundToEnd(round1);
await xAllocationPool
.connect(owner)
.setUnallocatedFundsReceiverAddress(await dynamicBaseAllocationPool.getAddress());
// Only app1 claims, app2 does not
await xAllocationPool.claim(round1, app1Id);
const allFundsClaimed = await xAllocationPool.allFundsClaimed(round1);
expect(allFundsClaimed).to.equal(false);
const canDistribute = await dynamicBaseAllocationPool.canDistributeDBARewards(round1);
expect(canDistribute).to.equal(false);
const DISTRIBUTOR_ROLE = await dynamicBaseAllocationPool.DISTRIBUTOR_ROLE();
await dynamicBaseAllocationPool.connect(owner).grantRole(DISTRIBUTOR_ROLE, owner.address);
// Should not be able to distribute yet
await expect(dynamicBaseAllocationPool.connect(owner).distributeDBARewards(round1, [app2Id])).to.be.revertedWith("DBAPool: Round invalid or not ready to distribute");
// After app2 claims, should be able to distribute
await xAllocationPool.claim(round1, app2Id);
const allFundsClaimedAfter = await xAllocationPool.allFundsClaimed(round1);
expect(allFundsClaimedAfter).to.equal(true);
const canDistributeAfter = await dynamicBaseAllocationPool.canDistributeDBARewards(round1);
expect(canDistributeAfter).to.equal(true);
const unallocatedAmount = await xAllocationPool.unallocatedFunds(round1);
expect(unallocatedAmount).to.equal(ethers.parseEther("2100"));
// Now distribution should succeed
const initialApp2Funds = await x2EarnRewardsPool.availableFunds(app2Id);
await dynamicBaseAllocationPool.connect(owner).distributeDBARewards(round1, [app2Id]);
const finalApp2Funds = await x2EarnRewardsPool.availableFunds(app2Id);
expect(finalApp2Funds - initialApp2Funds).to.equal(ethers.parseEther("2100"));
});
it("Should distribute DBA rewards when one app exceeds cap and generates unallocated funds for other eligible apps", async function () {
this.timeout(120000); // Increase timeout for complex test
const config = createLocalConfig();
config.EMISSIONS_CYCLE_DURATION = 10;
config.INITIAL_X_ALLOCATION = ethers.parseEther("10000");
config.X_ALLOCATION_POOL_APP_SHARES_MAX_CAP = 50; // 50% max cap so we can exceed it
const { dynamicBaseAllocationPool, owner, x2EarnApps, xAllocationPool, xAllocationVoting, emissions, b3tr, minterAccount, otherAccounts, veBetterPassport, creators, x2EarnRewardsPool, } = await getOrDeployContractInstances({
forceDeploy: true,
config,
});
// Bootstrap emissions
await bootstrapEmissions();
// Setup voters with VOT3 tokens
await veBetterPassport.whitelist(otherAccounts[0].address);
await veBetterPassport.whitelist(otherAccounts[1].address);
await veBetterPassport.whitelist(otherAccounts[2].address);
await veBetterPassport.whitelist(otherAccounts[3].address);
await veBetterPassport.whitelist(otherAccounts[4].address);
await veBetterPassport.toggleCheck(1);
await getVot3Tokens(otherAccounts[0], "10000");
await getVot3Tokens(otherAccounts[1], "10000");
await getVot3Tokens(otherAccounts[2], "10000");
await getVot3Tokens(otherAccounts[3], "10000");
await getVot3Tokens(otherAccounts[4], "10000");
// Create three apps - use different creators for each
const app1Id = ethers.keccak256(ethers.toUtf8Bytes("app1"));
const app2Id = ethers.keccak256(ethers.toUtf8Bytes("app2"));
const app3Id = ethers.keccak256(ethers.toUtf8Bytes("app3"));
await x2EarnApps
.connect(creators[0])
.submitApp(otherAccounts[5].address, otherAccounts[5].address, "app1", "metadataURI");
await x2EarnApps
.connect(creators[1])
.submitApp(otherAccounts[6].address, ot