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