UNPKG

@vechain/vebetterdao-contracts

Version:

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

686 lines 71.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const hardhat_1 = require("hardhat"); const chai_1 = require("chai"); const mocha_1 = require("mocha"); const helpers_1 = require("./helpers"); (0, mocha_1.describe)("RelayerRewardsPool - @shard18", function () { let relayerRewardsPool; let b3tr; let emissions; let xAllocationVoting; let minterAccount; let owner; let upgrader; let poolAdmin; let relayer1; let relayer2; let user1; let user2; let otherAccounts; const relayerRewardsPoolWithPreferredRelayer = () => relayerRewardsPool; // Main setup - used by most tests const setupContracts = async () => { const config = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true, }); if (!config) throw new Error("Failed to deploy contracts"); relayerRewardsPool = config.relayerRewardsPool; b3tr = config.b3tr; emissions = config.emissions; xAllocationVoting = config.xAllocationVoting; owner = config.owner; minterAccount = config.minterAccount; otherAccounts = config.otherAccounts; // Setup test accounts upgrader = otherAccounts[0]; poolAdmin = otherAccounts[1]; relayer1 = otherAccounts[2]; relayer2 = otherAccounts[3]; user1 = otherAccounts[4]; user2 = otherAccounts[5]; // Grant roles for testing await b3tr.connect(owner).grantRole(await b3tr.MINTER_ROLE(), owner.address); await b3tr.connect(owner).grantRole(await b3tr.MINTER_ROLE(), await emissions.getAddress()); await emissions.connect(minterAccount).bootstrap(); await emissions.connect(minterAccount).start(); await relayerRewardsPool.connect(owner).grantRole(await relayerRewardsPool.POOL_ADMIN_ROLE(), poolAdmin.address); }; (0, mocha_1.beforeEach)(async function () { await setupContracts(); }); (0, mocha_1.describe)("Deployment and Initialization", function () { (0, mocha_1.it)("should deploy with correct initial values", async function () { (0, chai_1.expect)(await relayerRewardsPool.version()).to.equal("3"); (0, chai_1.expect)(await relayerRewardsPool.getVoteWeight()).to.equal(3); (0, chai_1.expect)(await relayerRewardsPool.getClaimWeight()).to.equal(1); (0, chai_1.expect)(await relayerRewardsPool.getEarlyAccessBlocks()).to.equal(432000); (0, chai_1.expect)(await relayerRewardsPool.getRelayerFeePercentage()).to.equal(10); (0, chai_1.expect)(await relayerRewardsPool.getRelayerFeeDenominator()).to.equal(100); (0, chai_1.expect)(await relayerRewardsPool.getFeeCap()).to.equal(hardhat_1.ethers.parseEther("100")); }); (0, mocha_1.it)("should have correct role assignments", async function () { const DEFAULT_ADMIN_ROLE = await relayerRewardsPool.DEFAULT_ADMIN_ROLE(); const POOL_ADMIN_ROLE = await relayerRewardsPool.POOL_ADMIN_ROLE(); (0, chai_1.expect)(await relayerRewardsPool.hasRole(DEFAULT_ADMIN_ROLE, owner.address)).to.be.true; (0, chai_1.expect)(await relayerRewardsPool.hasRole(POOL_ADMIN_ROLE, owner.address)).to.be.true; (0, chai_1.expect)(await relayerRewardsPool.hasRole(POOL_ADMIN_ROLE, poolAdmin.address)).to.be.true; }); (0, mocha_1.it)("should revert initialization with zero addresses", async function () { const RelayerRewardsPoolFactory = await hardhat_1.ethers.getContractFactory("RelayerRewardsPool"); const pool = await RelayerRewardsPoolFactory.deploy(); await (0, chai_1.expect)(pool.initialize(hardhat_1.ethers.ZeroAddress, upgrader.address, await b3tr.getAddress(), await emissions.getAddress(), await xAllocationVoting.getAddress())).to.be.revertedWithCustomError(pool, "InvalidInitialization"); await (0, chai_1.expect)(pool.initialize(owner.address, hardhat_1.ethers.ZeroAddress, await b3tr.getAddress(), await emissions.getAddress(), await xAllocationVoting.getAddress())).to.be.revertedWithCustomError(pool, "InvalidInitialization"); await (0, chai_1.expect)(pool.initialize(owner.address, upgrader.address, hardhat_1.ethers.ZeroAddress, await emissions.getAddress(), await xAllocationVoting.getAddress())).to.be.revertedWithCustomError(pool, "InvalidInitialization"); await (0, chai_1.expect)(pool.initialize(owner.address, upgrader.address, await b3tr.getAddress(), await emissions.getAddress(), hardhat_1.ethers.ZeroAddress)).to.be.revertedWithCustomError(pool, "InvalidInitialization"); }); }); (0, mocha_1.describe)("Role Management", function () { (0, mocha_1.it)("should allow admin to grant and revoke pool admin role", async function () { const POOL_ADMIN_ROLE = await relayerRewardsPool.POOL_ADMIN_ROLE(); // Grant role await relayerRewardsPool.connect(owner).grantRole(POOL_ADMIN_ROLE, user1.address); (0, chai_1.expect)(await relayerRewardsPool.hasRole(POOL_ADMIN_ROLE, user1.address)).to.be.true; // Revoke role await relayerRewardsPool.connect(owner).revokeRole(POOL_ADMIN_ROLE, user1.address); (0, chai_1.expect)(await relayerRewardsPool.hasRole(POOL_ADMIN_ROLE, user1.address)).to.be.false; }); (0, mocha_1.it)("should not allow non-admin to grant roles", async function () { const POOL_ADMIN_ROLE = await relayerRewardsPool.POOL_ADMIN_ROLE(); await (0, chai_1.expect)(relayerRewardsPool.connect(user1).grantRole(POOL_ADMIN_ROLE, user2.address)).to.be.reverted; }); (0, mocha_1.it)("should allow pool admin to perform admin functions", async function () { // Pool admin should be able to register relayers await (0, chai_1.expect)(relayerRewardsPool.connect(poolAdmin).registerRelayer(relayer1.address)).to.not.be.reverted; }); }); (0, mocha_1.describe)("Contract Configuration", function () { (0, mocha_1.it)("should allow admin to update B3TR address", async function () { const newB3TRAddress = user1.address; // Using a dummy address for testing await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setB3TRAddress(newB3TRAddress)) .to.emit(relayerRewardsPool, "B3TRAddressUpdated") .withArgs(newB3TRAddress, await b3tr.getAddress()); }); (0, mocha_1.it)("should allow admin to update Emissions address", async function () { const newEmissionsAddress = user1.address; // Using a dummy address for testing await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setEmissionsAddress(newEmissionsAddress)) .to.emit(relayerRewardsPool, "EmissionsAddressUpdated") .withArgs(newEmissionsAddress, await emissions.getAddress()); }); (0, mocha_1.it)("should revert setting zero address for B3TR", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setB3TRAddress(hardhat_1.ethers.ZeroAddress)) .to.be.revertedWithCustomError(relayerRewardsPool, "InvalidParameter") .withArgs("b3trAddress"); }); (0, mocha_1.it)("should revert setting zero address for Emissions", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setEmissionsAddress(hardhat_1.ethers.ZeroAddress)) .to.be.revertedWithCustomError(relayerRewardsPool, "InvalidParameter") .withArgs("emissionsAddress"); }); (0, mocha_1.it)("should not allow non-admin to update addresses", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(user1).setB3TRAddress(user2.address)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); await (0, chai_1.expect)(relayerRewardsPool.connect(user1).setEmissionsAddress(user2.address)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); }); }); (0, mocha_1.describe)("Weight Management", function () { (0, mocha_1.it)("should allow admin to update vote weight", async function () { const newWeight = 5; await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setVoteWeight(newWeight)) .to.emit(relayerRewardsPool, "VoteWeightUpdated") .withArgs(newWeight, 3); // 3 is the initial vote weight (0, chai_1.expect)(await relayerRewardsPool.getVoteWeight()).to.equal(newWeight); }); (0, mocha_1.it)("should allow admin to update claim weight", async function () { const newWeight = 2; await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setClaimWeight(newWeight)) .to.emit(relayerRewardsPool, "ClaimWeightUpdated") .withArgs(newWeight, 1); // 1 is the initial claim weight (0, chai_1.expect)(await relayerRewardsPool.getClaimWeight()).to.equal(newWeight); }); (0, mocha_1.it)("should revert setting zero weight", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setVoteWeight(0)) .to.be.revertedWithCustomError(relayerRewardsPool, "InvalidParameter") .withArgs("voteWeight"); await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setClaimWeight(0)) .to.be.revertedWithCustomError(relayerRewardsPool, "InvalidParameter") .withArgs("claimWeight"); }); (0, mocha_1.it)("should not allow non-admin to update weights", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(user1).setVoteWeight(5)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); await (0, chai_1.expect)(relayerRewardsPool.connect(user1).setClaimWeight(2)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); }); }); (0, mocha_1.describe)("Relayer Registration", function () { (0, mocha_1.it)("should allow admin to register a relayer", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(owner).registerRelayer(relayer1.address)) .to.emit(relayerRewardsPool, "RelayerRegistered") .withArgs(relayer1.address); (0, chai_1.expect)(await relayerRewardsPool.isRegisteredRelayer(relayer1.address)).to.be.true; const registeredRelayers = await relayerRewardsPool.getRegisteredRelayers(); (0, chai_1.expect)(registeredRelayers).to.include(relayer1.address); }); (0, mocha_1.it)("should allow pool admin to register a relayer", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(poolAdmin).registerRelayer(relayer1.address)) .to.emit(relayerRewardsPool, "RelayerRegistered") .withArgs(relayer1.address); (0, chai_1.expect)(await relayerRewardsPool.isRegisteredRelayer(relayer1.address)).to.be.true; }); (0, mocha_1.it)("should revert registering zero address", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(owner).registerRelayer(hardhat_1.ethers.ZeroAddress)) .to.be.revertedWithCustomError(relayerRewardsPool, "InvalidParameter") .withArgs("relayer"); }); (0, mocha_1.it)("should revert registering already registered relayer", async function () { await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await (0, chai_1.expect)(relayerRewardsPool.connect(owner).registerRelayer(relayer1.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "RelayerAlreadyRegistered") .withArgs(relayer1.address); }); (0, mocha_1.it)("should allow admin to unregister a relayer", async function () { // First register await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); (0, chai_1.expect)(await relayerRewardsPool.isRegisteredRelayer(relayer1.address)).to.be.true; // Then unregister await (0, chai_1.expect)(relayerRewardsPool.connect(owner).unregisterRelayer(relayer1.address)) .to.emit(relayerRewardsPool, "RelayerUnregistered") .withArgs(relayer1.address); (0, chai_1.expect)(await relayerRewardsPool.isRegisteredRelayer(relayer1.address)).to.be.false; const registeredRelayers = await relayerRewardsPool.getRegisteredRelayers(); (0, chai_1.expect)(registeredRelayers).to.not.include(relayer1.address); }); (0, mocha_1.it)("should revert unregistering non-registered relayer", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(owner).unregisterRelayer(relayer1.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "RelayerNotRegistered") .withArgs(relayer1.address); }); (0, mocha_1.it)("should handle multiple relayer registrations correctly", async function () { await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); const registeredRelayers = await relayerRewardsPool.getRegisteredRelayers(); (0, chai_1.expect)(registeredRelayers).to.have.lengthOf(2); (0, chai_1.expect)(registeredRelayers).to.include(relayer1.address); (0, chai_1.expect)(registeredRelayers).to.include(relayer2.address); // Unregister one and check the array is properly updated await relayerRewardsPool.connect(owner).unregisterRelayer(relayer1.address); const updatedRelayers = await relayerRewardsPool.getRegisteredRelayers(); (0, chai_1.expect)(updatedRelayers).to.have.lengthOf(1); (0, chai_1.expect)(updatedRelayers).to.include(relayer2.address); (0, chai_1.expect)(updatedRelayers).to.not.include(relayer1.address); }); (0, mocha_1.it)("should allow anyone to register any address as relayer", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(user1).registerRelayer(relayer1.address)) .to.emit(relayerRewardsPool, "RelayerRegistered") .withArgs(relayer1.address); (0, chai_1.expect)(await relayerRewardsPool.isRegisteredRelayer(relayer1.address)).to.be.true; }); (0, mocha_1.it)("should not allow non-admin to unregister another relayer", async function () { await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await (0, chai_1.expect)(relayerRewardsPool.connect(user1).unregisterRelayer(relayer1.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "UnauthorizedUnregister") .withArgs(user1.address, relayer1.address); }); (0, mocha_1.it)("should allow anyone to self-register as relayer", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(user1).registerRelayer(user1.address)) .to.emit(relayerRewardsPool, "RelayerRegistered") .withArgs(user1.address); (0, chai_1.expect)(await relayerRewardsPool.isRegisteredRelayer(user1.address)).to.be.true; const registeredRelayers = await relayerRewardsPool.getRegisteredRelayers(); (0, chai_1.expect)(registeredRelayers).to.include(user1.address); }); (0, mocha_1.it)("should allow relayer to unregister themselves", async function () { await relayerRewardsPool.connect(relayer1).registerRelayer(relayer1.address); (0, chai_1.expect)(await relayerRewardsPool.isRegisteredRelayer(relayer1.address)).to.be.true; await (0, chai_1.expect)(relayerRewardsPool.connect(relayer1).unregisterRelayer(relayer1.address)) .to.emit(relayerRewardsPool, "RelayerUnregistered") .withArgs(relayer1.address); (0, chai_1.expect)(await relayerRewardsPool.isRegisteredRelayer(relayer1.address)).to.be.false; const registeredRelayers = await relayerRewardsPool.getRegisteredRelayers(); (0, chai_1.expect)(registeredRelayers).to.not.include(relayer1.address); }); }); (0, mocha_1.describe)("Preferred Relayer", function () { (0, mocha_1.beforeEach)(async function () { await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); }); (0, mocha_1.it)("should allow a user to set and clear their preferred relayer", async function () { await (0, chai_1.expect)(relayerRewardsPoolWithPreferredRelayer().connect(user1).setPreferredRelayer(relayer1.address)) .to.emit(relayerRewardsPool, "PreferredRelayerSet") .withArgs(user1.address, relayer1.address); (0, chai_1.expect)(await relayerRewardsPoolWithPreferredRelayer().getPreferredRelayer(user1.address)).to.equal(relayer1.address); await (0, chai_1.expect)(relayerRewardsPoolWithPreferredRelayer().connect(user1).setPreferredRelayer(hardhat_1.ethers.ZeroAddress)) .to.emit(relayerRewardsPool, "PreferredRelayerSet") .withArgs(user1.address, hardhat_1.ethers.ZeroAddress); (0, chai_1.expect)(await relayerRewardsPoolWithPreferredRelayer().getPreferredRelayer(user1.address)).to.equal(hardhat_1.ethers.ZeroAddress); }); (0, mocha_1.it)("should revert when setting an unregistered relayer as preferred", async function () { await (0, chai_1.expect)(relayerRewardsPoolWithPreferredRelayer().connect(user1).setPreferredRelayer(user2.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "RelayerNotRegistered") .withArgs(user2.address); }); (0, mocha_1.it)("should only allow the preferred relayer to vote during early access", async function () { const roundId = await xAllocationVoting.currentRoundId(); await relayerRewardsPoolWithPreferredRelayer().connect(user1).setPreferredRelayer(relayer1.address); await (0, chai_1.expect)(relayerRewardsPool.validateVoteDuringEarlyAccess(roundId, user1.address, relayer1.address)).to.not.be .reverted; await (0, chai_1.expect)(relayerRewardsPool.validateVoteDuringEarlyAccess(roundId, user1.address, relayer2.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "NotPreferredRelayer") .withArgs(relayer2.address, relayer1.address); }); (0, mocha_1.it)("should only allow the preferred relayer to claim during early access", async function () { const roundId = await xAllocationVoting.currentRoundId(); await relayerRewardsPoolWithPreferredRelayer().connect(user1).setPreferredRelayer(relayer1.address); await (0, chai_1.expect)(relayerRewardsPool.validateClaimDuringEarlyAccess(roundId, user1.address, relayer1.address)).to.not .be.reverted; await (0, chai_1.expect)(relayerRewardsPool.validateClaimDuringEarlyAccess(roundId, user1.address, relayer2.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "NotPreferredRelayer") .withArgs(relayer2.address, relayer1.address); }); (0, mocha_1.it)("should allow any registered relayer during early access when no preference is set", async function () { const roundId = await xAllocationVoting.currentRoundId(); await (0, chai_1.expect)(relayerRewardsPool.validateVoteDuringEarlyAccess(roundId, user1.address, relayer1.address)).to.not.be .reverted; await (0, chai_1.expect)(relayerRewardsPool.validateVoteDuringEarlyAccess(roundId, user1.address, relayer2.address)).to.not.be .reverted; await (0, chai_1.expect)(relayerRewardsPool.validateClaimDuringEarlyAccess(roundId, user1.address, relayer1.address)).to.not .be.reverted; await (0, chai_1.expect)(relayerRewardsPool.validateClaimDuringEarlyAccess(roundId, user1.address, relayer2.address)).to.not .be.reverted; }); (0, mocha_1.it)("should fall back to any registered relayer when the preferred relayer is unregistered", async function () { const roundId = await xAllocationVoting.currentRoundId(); await relayerRewardsPoolWithPreferredRelayer().connect(user1).setPreferredRelayer(relayer1.address); await relayerRewardsPool.connect(relayer1).unregisterRelayer(relayer1.address); await (0, chai_1.expect)(relayerRewardsPool.validateVoteDuringEarlyAccess(roundId, user1.address, relayer2.address)).to.not.be .reverted; await (0, chai_1.expect)(relayerRewardsPool.validateClaimDuringEarlyAccess(roundId, user1.address, relayer2.address)).to.not .be.reverted; }); (0, mocha_1.it)("should allow anyone after early access even when a preferred relayer is set", async function () { const roundId = await xAllocationVoting.currentRoundId(); await relayerRewardsPoolWithPreferredRelayer().connect(user1).setPreferredRelayer(relayer1.address); await relayerRewardsPool.connect(owner).setEarlyAccessBlocks(0); await (0, helpers_1.waitForNextCycle)(emissions); await (0, chai_1.expect)(relayerRewardsPool.validateVoteDuringEarlyAccess(roundId, user1.address, user2.address)).to.not.be .reverted; await (0, chai_1.expect)(relayerRewardsPool.validateClaimDuringEarlyAccess(roundId, user1.address, user2.address)).to.not.be .reverted; }); }); (0, mocha_1.describe)("Early Access Configuration", function () { (0, mocha_1.it)("should allow admin to update early access blocks", async function () { const newBlocks = 200; await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setEarlyAccessBlocks(newBlocks)) .to.emit(relayerRewardsPool, "EarlyAccessBlocksUpdated") .withArgs(newBlocks, 432000); // 432000 is the initial value (0, chai_1.expect)(await relayerRewardsPool.getEarlyAccessBlocks()).to.equal(newBlocks); }); (0, mocha_1.it)("should not allow non-admin to update early access blocks", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(user1).setEarlyAccessBlocks(200)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); }); }); (0, mocha_1.describe)("Round Setup and Action Management", function () { (0, mocha_1.beforeEach)(async function () { // Register relayers for testing await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); }); (0, mocha_1.it)("should set total actions for round correctly", async function () { const roundId = 1; const totalAutoVotingUsers = 10; await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers)) .to.emit(relayerRewardsPool, "TotalAutoVotingActionsSet") .withArgs(roundId, totalAutoVotingUsers, totalAutoVotingUsers * 2, totalAutoVotingUsers * 2 * 2, 2); // 2 registered relayers (0, chai_1.expect)(await relayerRewardsPool.totalActions(roundId)).to.equal(totalAutoVotingUsers * 2); // 2 actions per user const voteWeight = await relayerRewardsPool.getVoteWeight(); const claimWeight = await relayerRewardsPool.getClaimWeight(); const expectedWeightedActions = BigInt(totalAutoVotingUsers) * (voteWeight + claimWeight); (0, chai_1.expect)(await relayerRewardsPool.totalWeightedActions(roundId)).to.equal(expectedWeightedActions); }); (0, mocha_1.it)("should register relayer actions correctly", async function () { const roundId = 1; const totalAutoVotingUsers = 10; await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); const voteWeight = await relayerRewardsPool.getVoteWeight(); const claimWeight = await relayerRewardsPool.getClaimWeight(); // Register a VOTE action await (0, chai_1.expect)(relayerRewardsPool.connect(owner).registerRelayerAction(relayer1.address, user1.address, roundId, 0)) // 0 = VOTE .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user1.address, roundId, 1, voteWeight); (0, chai_1.expect)(await relayerRewardsPool.totalRelayerActions(relayer1.address, roundId)).to.equal(1); (0, chai_1.expect)(await relayerRewardsPool.totalRelayerWeightedActions(relayer1.address, roundId)).to.equal(voteWeight); // Register a CLAIM action await (0, chai_1.expect)(relayerRewardsPool.connect(owner).registerRelayerAction(relayer1.address, user1.address, roundId, 1)) // 1 = CLAIM .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user1.address, roundId, 2, claimWeight); (0, chai_1.expect)(await relayerRewardsPool.totalRelayerActions(relayer1.address, roundId)).to.equal(2); (0, chai_1.expect)(await relayerRewardsPool.totalRelayerWeightedActions(relayer1.address, roundId)).to.equal(voteWeight + claimWeight); }); (0, mocha_1.it)("should revert registering action for zero address", async function () { const roundId = 1; await (0, chai_1.expect)(relayerRewardsPool.connect(owner).registerRelayerAction(hardhat_1.ethers.ZeroAddress, user1.address, roundId, 0)) .to.be.revertedWithCustomError(relayerRewardsPool, "InvalidParameter") .withArgs("relayer"); }); (0, mocha_1.it)("should not allow non-admin to register actions or set round totals", async function () { const roundId = 1; await (0, chai_1.expect)(relayerRewardsPool.connect(user1).setTotalActionsForRound(roundId, 10)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); await (0, chai_1.expect)(relayerRewardsPool.connect(user1).registerRelayerAction(relayer1.address, user1.address, roundId, 0)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); }); }); (0, mocha_1.describe)("Reward Deposits", function () { (0, mocha_1.it)("should allow admin to deposit rewards", async function () { const roundId = 1; const amount = hardhat_1.ethers.parseEther("100"); // Give B3TR tokens to owner await b3tr.connect(owner).mint(owner.address, amount); await b3tr.connect(owner).approve(await relayerRewardsPool.getAddress(), amount); await (0, chai_1.expect)(relayerRewardsPool.connect(owner).deposit(amount, roundId)) .to.emit(relayerRewardsPool, "RewardsDeposited") .withArgs(roundId, amount, amount); // First deposit, so total equals amount (0, chai_1.expect)(await relayerRewardsPool.getTotalRewards(roundId)).to.equal(amount); }); (0, mocha_1.it)("should accumulate deposits for the same round", async function () { const roundId = 1; const amount1 = hardhat_1.ethers.parseEther("100"); const amount2 = hardhat_1.ethers.parseEther("50"); // Give B3TR tokens to owner await b3tr.connect(owner).mint(owner.address, amount1 + amount2); await b3tr.connect(owner).approve(await relayerRewardsPool.getAddress(), amount1 + amount2); await relayerRewardsPool.connect(owner).deposit(amount1, roundId); await relayerRewardsPool.connect(owner).deposit(amount2, roundId); (0, chai_1.expect)(await relayerRewardsPool.getTotalRewards(roundId)).to.equal(amount1 + amount2); }); (0, mocha_1.it)("should revert deposit with zero amount", async function () { await (0, chai_1.expect)(relayerRewardsPool.connect(owner).deposit(0, 1)) .to.be.revertedWithCustomError(relayerRewardsPool, "InvalidParameter") .withArgs("amount"); }); (0, mocha_1.it)("should not allow non-admin to deposit", async function () { const amount = hardhat_1.ethers.parseEther("100"); await (0, chai_1.expect)(relayerRewardsPool.connect(user1).deposit(amount, 1)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); }); }); (0, mocha_1.describe)("Reward Claiming", function () { // Fresh setup for this specific test suite (0, mocha_1.beforeEach)(async function () { // Get completely fresh contracts for this test suite await setupContracts(); // End emissions explicitly await (0, helpers_1.waitForNextCycle)(emissions); // Setup for reward claiming tests await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); const roundId = 1; const totalAutoVotingUsers = 4; const rewardAmount = hardhat_1.ethers.parseEther("100"); // Set up round await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); // Deposit rewards await b3tr.connect(owner).mint(owner.address, rewardAmount); await b3tr.connect(owner).approve(await relayerRewardsPool.getAddress(), rewardAmount); await relayerRewardsPool.connect(owner).deposit(rewardAmount, roundId); // Each relayer completes exactly half the required actions (2 users worth each) await relayerRewardsPool.connect(owner).registerRelayerAction(relayer1.address, user1.address, roundId, 0); // VOTE await relayerRewardsPool.connect(owner).registerRelayerAction(relayer1.address, user1.address, roundId, 1); // CLAIM await relayerRewardsPool.connect(owner).registerRelayerAction(relayer1.address, user1.address, roundId, 0); // VOTE await relayerRewardsPool.connect(owner).registerRelayerAction(relayer1.address, user1.address, roundId, 1); // CLAIM await relayerRewardsPool.connect(owner).registerRelayerAction(relayer2.address, user2.address, roundId, 0); // VOTE await relayerRewardsPool.connect(owner).registerRelayerAction(relayer2.address, user2.address, roundId, 1); // CLAIM await relayerRewardsPool.connect(owner).registerRelayerAction(relayer2.address, user2.address, roundId, 0); // VOTE await relayerRewardsPool.connect(owner).registerRelayerAction(relayer2.address, user2.address, roundId, 1); // CLAIM }); (0, mocha_1.it)("should calculate claimable rewards correctly", async function () { const roundId = 1; const rewardAmount = hardhat_1.ethers.parseEther("100"); // VERIFY: All required actions are now completed const totalWeightedActions = await relayerRewardsPool.totalWeightedActions(roundId); const completedWeightedActions = await relayerRewardsPool.completedWeightedActions(roundId); (0, chai_1.expect)(completedWeightedActions).to.equal(totalWeightedActions, "All actions should be completed"); // THEN: Both conditions for claimable rewards are met: // 1. All actions completed // 2. Emission cycle ended (0, chai_1.expect)(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; // THEN: Both relayers should get equal rewards (they did equal work) const relayer1Claimable = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); const relayer2Claimable = await relayerRewardsPool.claimableRewards(relayer2.address, roundId); (0, chai_1.expect)(relayer1Claimable).to.equal(relayer2Claimable, "Relayers did equal work, should get equal rewards"); (0, chai_1.expect)(relayer1Claimable + relayer2Claimable).to.equal(rewardAmount, "Total claimable should equal deposited rewards"); (0, chai_1.expect)(relayer1Claimable).to.be.gt(0, "Rewards should be greater than zero"); }); (0, mocha_1.it)("should allow relayer to claim rewards when round is complete", async function () { const roundId = 1; const claimableAmount = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); (0, chai_1.expect)(claimableAmount).to.be.gt(0); const initialBalance = await b3tr.balanceOf(relayer1.address); await (0, chai_1.expect)(relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address)) .to.emit(relayerRewardsPool, "RelayerRewardsClaimed") .withArgs(relayer1.address, roundId, claimableAmount); const finalBalance = await b3tr.balanceOf(relayer1.address); (0, chai_1.expect)(finalBalance - initialBalance).to.equal(claimableAmount); // Should not be able to claim again (0, chai_1.expect)(await relayerRewardsPool.claimableRewards(relayer1.address, roundId)).to.equal(0); }); (0, mocha_1.it)("should allow all relayers to claim rewards", async function () { const roundId = 1; const initialBalance = await b3tr.balanceOf(relayer1.address); const claimableAmount = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); const initialBalance2 = await b3tr.balanceOf(relayer2.address); const claimableAmount2 = await relayerRewardsPool.claimableRewards(relayer2.address, roundId); // User1 claims for relayer1 await (0, chai_1.expect)(relayerRewardsPool.connect(user1).claimRewards(roundId, relayer1.address)) .to.emit(relayerRewardsPool, "RelayerRewardsClaimed") .withArgs(relayer1.address, roundId, claimableAmount); await (0, chai_1.expect)(relayerRewardsPool.connect(user1).claimRewards(roundId, relayer2.address)) .to.emit(relayerRewardsPool, "RelayerRewardsClaimed") .withArgs(relayer2.address, roundId, claimableAmount2); // Relayer1 and Relayer2 should receive the rewards const finalBalance = await b3tr.balanceOf(relayer1.address); const finalBalance2 = await b3tr.balanceOf(relayer2.address); (0, chai_1.expect)(finalBalance - initialBalance).to.equal(claimableAmount); (0, chai_1.expect)(finalBalance2 - initialBalance2).to.equal(claimableAmount2); // Should not be able to claim again await (0, chai_1.expect)(relayerRewardsPool.connect(user1).claimRewards(roundId, relayer1.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "RewardsAlreadyClaimed") .withArgs(relayer1.address, roundId); await (0, chai_1.expect)(relayerRewardsPool.connect(user1).claimRewards(roundId, relayer2.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "RewardsAlreadyClaimed") .withArgs(relayer2.address, roundId); }); (0, mocha_1.it)("should revert claiming when round is not complete", async function () { const incompleteRoundId = 2; const totalAutoVotingUsers = 4; // Start a second round await emissions.connect(minterAccount).distribute(); // Set up a new round that will be incomplete await relayerRewardsPool.connect(owner).setTotalActionsForRound(incompleteRoundId, totalAutoVotingUsers); // Only register SOME actions (not all required) await relayerRewardsPool .connect(owner) .registerRelayerAction(relayer1.address, user1.address, incompleteRoundId, 0); // VOTE await relayerRewardsPool .connect(owner) .registerRelayerAction(relayer1.address, user1.address, incompleteRoundId, 1); // CLAIM // Verify round is not claimable (0, chai_1.expect)(await relayerRewardsPool.isRewardClaimable(incompleteRoundId)).to.be.false; (0, chai_1.expect)(await relayerRewardsPool.claimableRewards(relayer1.address, incompleteRoundId)).to.equal(0); // Should revert when trying to claim await (0, chai_1.expect)(relayerRewardsPool.connect(relayer1).claimRewards(incompleteRoundId, relayer1.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "RoundNotEnded") .withArgs(incompleteRoundId); }); (0, mocha_1.it)("should revert when all actions are not completed", async function () { const incompleteRoundId = 2; const totalAutoVotingUsers = 4; // Start a second round await emissions.connect(minterAccount).distribute(); // Set up a new round that will be incomplete await relayerRewardsPool.connect(owner).setTotalActionsForRound(incompleteRoundId, totalAutoVotingUsers); // Only register SOME actions (not all required) await relayerRewardsPool .connect(owner) .registerRelayerAction(relayer1.address, user1.address, incompleteRoundId, 0); // VOTE await relayerRewardsPool .connect(owner) .registerRelayerAction(relayer1.address, user1.address, incompleteRoundId, 1); // CLAIM await (0, helpers_1.waitForNextCycle)(emissions); // Verify round is not claimable (0, chai_1.expect)(await relayerRewardsPool.isRewardClaimable(incompleteRoundId)).to.be.false; (0, chai_1.expect)(await relayerRewardsPool.claimableRewards(relayer1.address, incompleteRoundId)).to.equal(0); // Should revert when trying to claim await (0, chai_1.expect)(relayerRewardsPool.connect(relayer1).claimRewards(incompleteRoundId, relayer1.address)) .to.be.revertedWithCustomError(relayerRewardsPool, "NoRewardsToClaim") .withArgs(relayer1.address, incompleteRoundId); }); }); (0, mocha_1.describe)("Reward Claimability", function () { (0, mocha_1.it)("should correctly determine if rewards are claimable", async function () { const incompleteRoundId = 1; const totalAutoVotingUsers = 2; await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await relayerRewardsPool.connect(owner).setTotalActionsForRound(incompleteRoundId, totalAutoVotingUsers); // Initially not claimable (no actions completed) (0, chai_1.expect)(await relayerRewardsPool.isRewardClaimable(incompleteRoundId)).to.be.false; // Complete half the actions await relayerRewardsPool .connect(owner) .registerRelayerAction(relayer1.address, user1.address, incompleteRoundId, 0); // VOTE await relayerRewardsPool .connect(owner) .registerRelayerAction(relayer1.address, user1.address, incompleteRoundId, 1); // CLAIM // Still not claimable (only half completed) (0, chai_1.expect)(await relayerRewardsPool.isRewardClaimable(incompleteRoundId)).to.be.false; // Complete all actions await relayerRewardsPool .connect(owner) .registerRelayerAction(relayer1.address, user1.address, incompleteRoundId, 0); // VOTE await relayerRewardsPool .connect(owner) .registerRelayerAction(relayer1.address, user1.address, incompleteRoundId, 1); // CLAIM // Now should be claimable await (0, helpers_1.waitForNextCycle)(emissions); (0, chai_1.expect)(await relayerRewardsPool.isRewardClaimable(incompleteRoundId)).to.be.true; }); }); (0, mocha_1.describe)("Edge Cases and Error Handling", function () { (0, mocha_1.it)("should handle zero relayers in total actions for round", async function () { const roundId = 1; const totalAutoVotingUsers = 10; await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers)) .to.emit(relayerRewardsPool, "TotalAutoVotingActionsSet") .withArgs(roundId, totalAutoVotingUsers, totalAutoVotingUsers * 2, totalAutoVotingUsers * 2 * 2, 0); // Total actions should still be set correctly (0, chai_1.expect)(await relayerRewardsPool.totalActions(roundId)).to.equal(totalAutoVotingUsers * 2); }); (0, mocha_1.it)("should handle proportional rewards correctly with different weighted actions", async function () { const roundId = 1; const totalAutoVotingUsers = 2; const rewardAmount = hardhat_1.ethers.parseEther("100"); await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); // Deposit rewards await b3tr.connect(owner).mint(owner.address, rewardAmount); await b3tr.connect(owner).approve(await relayerRewardsPool.getAddress(), rewardAmount); await relayerRewardsPool.connect(owner).deposit(rewardAmount, roundId); // Relayer1 does more VOTE actions (higher weight) await relayerRewardsPool.connect(owner).registerRelayerAction(relayer1.address, user1.address, roundId, 0); // VOTE (weight 3) await relayerRewardsPool.connect(owner).registerRelayerAction(relayer1.address, user1.address, roundId, 0); // VOTE (weight 3) // Relayer2 does CLAIM actions (lower weight) await relayerRewardsPool.connect(owner).registerRelayerAction(relayer2.address, user2.address, roundId, 1); // CLAIM (weight 1) await relayerRewardsPool.connect(owner).registerRelayerAction(relayer2.address, user2.address, roundId, 1); // CLAIM (weight 1) await (0, helpers_1.waitForNextCycle)(emissions); const relayer1Claimable = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); const relayer2Claimable = await relayerRewardsPool.claimableRewards(relayer2.address, roundId); // Relayer1 should get more rewards due to higher weighted actions (0, chai_1.expect)(relayer1Claimable).to.be.gt(relayer2Claimable); (0, chai_1.expect)(relayer1Claimable + relayer2Claimable).to.equal(rewardAmount); }); (0, mocha_1.it)("should handle maximum values correctly", async function () { const roundId = 1; // This should not overflow await (0, chai_1.expect)(relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, 1000)).to.not.be.reverted; }); }); (0, mocha_1.describe)("Reduce Expected Actions", function () { (0, mocha_1.beforeEach)(async function () { await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); }); (0, mocha_1.it)("should reduce expected actions for round correctly", async function () { const roundId = 1; const totalAutoVotingUsers = 10; const usersToReduce = 3; // Set initial total actions await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); const initialTotalActions = await relayerRewardsPool.totalActions(roundId); const initialTotalWeightedActions = await relayerRewardsPool.totalWeightedActions(roundId); // Calculate expected reductions const actionsToReduce = usersToReduce * 2; // Each user requires 2 actions (vote + claim) const voteWeight = await relayerRewardsPool.getVoteWeight(); const claimWeight = await relayerRewardsPool.getClaimWeight(); const weightedActionsToReduce = BigInt(usersToReduce) * (voteWeight + claimWeight); const expectedNewTotalActions = initialTotalActions - BigInt(actionsToReduce); const expectedNewTotalWeightedActions = initialTotalWeightedActions - weightedActionsToReduce; // Reduce expected actions (event reports plain actions removed, e.g. 2 per user for full allocation skip) await (0, chai_1.expect)(relayerRewardsPool.connect(owner).reduceExpectedActionsForRound(roundId, usersToReduce)) .to.emit(relayerRewardsPool, "ExpectedActionsReduced") .withArgs(roundId, actionsToReduce, expectedNewTotalActions, expectedNewTotalWeightedActions); // Verify the totals are updated correctly (0, chai_1.expect)(await relayerRewardsPool.totalActions(roundId)).to.equal(expectedNewTotalActions); (0, chai_1.expect)(await relayerRewardsPool.totalWeightedActions(roundId)).to.equal(expectedNewTotalWeightedActions); }); (0, mocha_1.it)("should allow pool admin to reduce expected actions", async function () { const roundId = 1; const totalAutoVotingUsers = 5; const usersToReduce = 1; await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); await (0, chai_1.expect)(relayerRewardsPool.connect(poolAdmin).reduceExpectedActionsForRound(roundId, usersToReduce)).to.not .be.reverted; }); (0, mocha_1.it)("should revert when trying to reduce zero users", async function () { const roundId = 1; await (0, chai_1.expect)(relayerRewardsPool.connect(owner).reduceExpectedActionsForRound(roundId, 0)) .to.be.revertedWithCustomError(relayerRewardsPool, "InvalidParameter") .withArgs("userCount"); }); (0, mocha_1.it)("should revert when trying to reduce more actions than available", async function () { const roundId = 1; const totalAutoVotingUsers = 2; const usersToReduce = 5; // More than available await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); await (0, chai_1.expect)(relayerRewardsPool.connect(owner).reduceExpectedActionsForRound(roundId, usersToReduce)).to.be.revertedWith("RelayerRewardsPool: cannot reduce more actions than available"); }); (0, mocha_1.it)("should revert when trying to reduce more weighted actions than available", async function () { const roundId = 1; const totalAutoVotingUsers = 1; const usersToReduce = 2; // More than available await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); await (0, chai_1.expect)(relayerRewardsPool.connect(owner).reduceExpectedActionsForRound(roundId, usersToReduce)).to.be.revertedWith("RelayerRewardsPool: cannot reduce more actions than available"); }); (0, mocha_1.it)("should not allow non-admin to reduce expected actions", async function () { const roundId = 1; const usersToReduce = 1; await (0, chai_1.expect)(relayerRewardsPool.connect(user1).reduceExpectedActionsForRound(roundId, usersToReduce)).to.be.revertedWith("RelayerRewardsPool: caller must have admin or pool admin role"); }); (0, mocha_1.it)("should handle reducing all users correctly", async function () { const roundId = 1; const totalAutoVotingUsers = 3; await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); // Reduce all users await relayerRewardsPool.connect(owner).reduceExpectedActionsForRound(roundId, totalAutoVotingUsers); // Should result in zero expected actions (0, chai_1.expect)(await relayerRewardsPool.totalActions(roundId)).to.equal(0); (0, chai_1.expect)(await relayerRewardsPool.totalWeightedActions(roundId)).to.equal(0); }); (0, mocha_1.it)("should allow multiple reductions correctly", async function () { const roundId = 1; const totalAutoVotingUsers = 10; await relayerRewardsPool.connect(owner).setTotalActionsForRound(roundId, totalAutoVotingUsers); const voteWeight = await relayerRewardsPool.getVoteWeight(); const claimWeight = await relayerRewardsPool.getClaimWeight(); // First reduction const firstReduction = 3; await relayerRewardsPool.connect(owner).reduceExpectedActionsForRound(roundId, firstReduction); const afterFirstTotalAction