UNPKG

@vechain/vebetterdao-contracts

Version:

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

514 lines (513 loc) 36.3 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"); const xnodes_1 = require("../../helpers/xnodes"); (0, mocha_1.describe)("VoterRewards V6 - @shard10b", function () { let xAllocationVoting; let x2EarnApps; let veBetterPassport; let voterRewards; let b3tr; let vot3; let emissions; let relayerRewardsPool; let owner; let relayer1; let minterAccount; let otherAccounts; let user; let user1; let user2; let appOwner; // 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"); xAllocationVoting = config.xAllocationVoting; x2EarnApps = config.x2EarnApps; veBetterPassport = config.veBetterPassport; voterRewards = config.voterRewards; b3tr = config.b3tr; vot3 = config.vot3; emissions = config.emissions; owner = config.owner; minterAccount = config.minterAccount; otherAccounts = config.otherAccounts; relayerRewardsPool = config.relayerRewardsPool; relayer1 = otherAccounts[0]; user = otherAccounts[1]; user1 = otherAccounts[2]; user2 = otherAccounts[3]; appOwner = otherAccounts[4]; await b3tr.connect(owner).grantRole(await b3tr.MINTER_ROLE(), await emissions.getAddress()); await emissions.connect(minterAccount).bootstrap(); await relayerRewardsPool.connect(owner).setRelayerFeePercentage(10); await voterRewards.connect(owner).setXAllocationVoting(await xAllocationVoting.getAddress()); await veBetterPassport.toggleCheck(1); await veBetterPassport.whitelist(user.address); await veBetterPassport.whitelist(user1.address); await veBetterPassport.whitelist(user2.address); await (0, helpers_1.getVot3Tokens)(user, "1000"); await (0, helpers_1.getVot3Tokens)(user1, "1000"); await (0, helpers_1.getVot3Tokens)(user2, "1000"); await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); }; (0, mocha_1.describe)("Relayer Fee Functionality", function () { (0, mocha_1.beforeEach)(async function () { await setupContracts(); await emissions.connect(minterAccount).start(); }); (0, mocha_1.it)("should take a fee when a relayer claims for a user with auto-voting enabled", async function () { // Create a test app const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(owner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); // Wait for next cycle and start emissions await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Enable auto-voting for user await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Start a new round await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // Auto-vote for user via relayer const txVote = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); const voteWeight = await relayerRewardsPool.getVoteWeight(); await (0, chai_1.expect)(txVote) .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user.address, roundId, 1, voteWeight); // Wait for the round to end await (0, helpers_1.waitForRoundToEnd)(roundId); // Check initial balances const initialRelayerBalance = await b3tr.balanceOf(relayer1.address); const initialUserBalance = await b3tr.balanceOf(user.address); const initialPoolTotal = await relayerRewardsPool.getTotalRewards(roundId); const claimWeight = await relayerRewardsPool.getClaimWeight(); // Get the expected rewards const userReward = await voterRewards.getReward(roundId, user.address); const userGMReward = await voterRewards.getGMReward(roundId, user.address); const userTotalReward = userReward + userGMReward; // Calculate expected fee (10% of total reward) const expectedFee = await voterRewards.getRelayerFee(roundId, user.address); // Relayer claims for user (who has auto-voting enabled) const tx = await voterRewards.connect(relayer1).claimReward(roundId, user.address); // Relayer action registered in pool and fee deposited await (0, chai_1.expect)(tx) .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user.address, roundId, 2, claimWeight); await (0, chai_1.expect)(tx) .to.emit(relayerRewardsPool, "RewardsDeposited") .withArgs(roundId, expectedFee, initialPoolTotal + expectedFee); // Check balances after claim const relayerBalanceAfter = await b3tr.balanceOf(relayer1.address); const userBalanceAfter = await b3tr.balanceOf(user.address); // Relayer should NOT receive the fee directly (0, chai_1.expect)(relayerBalanceAfter - initialRelayerBalance).to.equal(0n); // User should have received the reward minus the fee (0, chai_1.expect)(userBalanceAfter - initialUserBalance).to.equal(userTotalReward); }); (0, mocha_1.it)("should not take a fee when a relayer claims for a user without auto-voting enabled", async function () { // Create a test app const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(owner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); // Wait for next cycle and start emissions await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // User votes manually with enough tokens to meet the threshold const votingThreshold = await xAllocationVoting.votingThreshold(); const voteAmount = votingThreshold; await xAllocationVoting.connect(user).castVote(roundId, [app1Id], [voteAmount]); // Wait for the round to end await (0, helpers_1.waitForRoundToEnd)(roundId); // Check initial balances const initialRelayerBalance = await b3tr.balanceOf(relayer1.address); const initialUserBalance = await b3tr.balanceOf(user.address); // Get the expected rewards const userReward = await voterRewards.getReward(roundId, user.address); const userGMReward = await voterRewards.getGMReward(roundId, user.address); const userTotalReward = userReward + userGMReward; // Relayer claims for user (who doesn't have auto-voting enabled) const tx = await voterRewards.connect(relayer1).claimReward(roundId, user.address); // Should emit RewardClaimedV2 event await (0, chai_1.expect)(tx) .to.emit(voterRewards, "RewardClaimedV2") .withArgs(roundId, user.address, userReward, userGMReward); // Should NOT emit RelayerActionRegistered and RewardsDeposited events await (0, chai_1.expect)(tx).to.not.emit(relayerRewardsPool, "RelayerActionRegistered"); await (0, chai_1.expect)(tx).to.not.emit(relayerRewardsPool, "RewardsDeposited"); // Check balances after claim const relayerBalanceAfter = await b3tr.balanceOf(relayer1.address); const userBalanceAfter = await b3tr.balanceOf(user.address); // Relayer balance should not change (0, chai_1.expect)(relayerBalanceAfter).to.equal(initialRelayerBalance); // User should have received the full reward (0, chai_1.expect)(userBalanceAfter - initialUserBalance).to.equal(userTotalReward); }); (0, mocha_1.it)("should revert when user claims their own rewards during early access period if auto-voting is enabled", async function () { // Create a test app const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(owner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); // Wait for next cycle and start emissions await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Enable auto-voting for user await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Start a new round await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // Auto-vote for user await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); // Wait for the round to end await (0, helpers_1.waitForRoundToEnd)(roundId); // User claims their own rewards await (0, chai_1.expect)(voterRewards.connect(user).claimReward(roundId, user.address)).to.be.revertedWith("RelayerRewardsPool: auto-voting users cannot claim for themselves during early access period"); }); (0, mocha_1.it)("should handle fee for auto-voting and non-auto-voting users", async function () { // Create a test app const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(owner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); // Wait for next cycle and start emissions await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Enable auto-voting for user await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Start a new round await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // Auto-vote for user const txVote = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); const voteWeight = await relayerRewardsPool.getVoteWeight(); await (0, chai_1.expect)(txVote) .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user.address, roundId, 1, voteWeight); // Cast vote for user1 manually await xAllocationVoting.connect(user1).castVote(roundId, [app1Id], [hardhat_1.ethers.parseEther("1000")]); // Wait for the round to end await (0, helpers_1.waitForRoundToEnd)(roundId); // Get the expected rewards const userReward = await voterRewards.getReward(roundId, user.address); const userGMReward = await voterRewards.getGMReward(roundId, user.address); const userTotalReward = userReward + userGMReward; const user1Reward = await voterRewards.getReward(roundId, user1.address); const user1GMReward = await voterRewards.getGMReward(roundId, user1.address); const user1TotalReward = user1Reward + user1GMReward; // 10% fee for auto-voting user const expectedFee = await voterRewards.getRelayerFee(roundId, user.address); // No fee for user1 because auto-voting is disabled const expectedUser1Fee = await voterRewards.getRelayerFee(roundId, user1.address); // Initial balances const initialRelayerBalance = await b3tr.balanceOf(relayer1.address); const initialUserBalance = await b3tr.balanceOf(user.address); const initialPoolTotal = await relayerRewardsPool.getTotalRewards(roundId); const claimWeight = await relayerRewardsPool.getClaimWeight(); // Relayer claims for user const tx = await voterRewards.connect(relayer1).claimReward(roundId, user.address); // Action should be registered in the pool and fee deposited await (0, chai_1.expect)(tx) .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user.address, roundId, 2, claimWeight); await (0, chai_1.expect)(tx) .to.emit(relayerRewardsPool, "RewardsDeposited") .withArgs(roundId, expectedFee, initialPoolTotal + expectedFee); // Check balances after claim const relayerBalanceAfter = await b3tr.balanceOf(relayer1.address); const userBalanceAfter = await b3tr.balanceOf(user.address); // Relayer should NOT receive the fee directly (0, chai_1.expect)(relayerBalanceAfter - initialRelayerBalance).to.equal(0n); (0, chai_1.expect)(userBalanceAfter - initialUserBalance).to.equal(userTotalReward); // Fee should be deposited to the pool const poolTotalAfter = await relayerRewardsPool.getTotalRewards(roundId); (0, chai_1.expect)(poolTotalAfter - initialPoolTotal).to.equal(expectedFee); // User1 should have received the full reward await voterRewards.connect(user1).claimReward(roundId, user1.address); const user1BalanceAfter = await b3tr.balanceOf(user1.address); (0, chai_1.expect)(user1BalanceAfter).to.equal(user1TotalReward); (0, chai_1.expect)(expectedUser1Fee).to.equal(0n); }); (0, mocha_1.it)("should handle fee for relayer claiming their own rewards", async function () { // Setup additional relayer const relayer2 = otherAccounts[5]; await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); // Create a test app const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(owner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); // Wait for next cycle and start emissions await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Enable auto-voting for both users await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await xAllocationVoting.connect(user1).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user1).toggleAutoVoting(user1.address); // Start a new round await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // Relayer1 auto-votes for user (earns VOTE action) await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); // Relayer2 auto-votes for user1 (earns VOTE action) await xAllocationVoting.connect(relayer2).castVoteOnBehalfOf(user1.address, roundId); // Wait for the round to end await (0, helpers_1.waitForRoundToEnd)(roundId); // Get initial pool state const initialPoolTotal = await relayerRewardsPool.getTotalRewards(roundId); const claimWeight = await relayerRewardsPool.getClaimWeight(); // Relayer1 claims for user (earns CLAIM action) const userFee = await voterRewards.getRelayerFee(roundId, user.address); const tx1 = await voterRewards.connect(relayer1).claimReward(roundId, user.address); await (0, chai_1.expect)(tx1) .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user.address, roundId, 2, claimWeight); // 2nd action for relayer1 // Relayer2 claims for user1 (earns CLAIM action) const user1Fee = await voterRewards.getRelayerFee(roundId, user1.address); const tx2 = await voterRewards.connect(relayer2).claimReward(roundId, user1.address); await (0, chai_1.expect)(tx2) .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer2.address, user1.address, roundId, 2, claimWeight); // 2nd action for relayer2 // Check that fees were deposited to the pool const poolTotalAfterClaims = await relayerRewardsPool.getTotalRewards(roundId); const totalFeesDeposited = userFee + user1Fee; (0, chai_1.expect)(poolTotalAfterClaims - initialPoolTotal).to.equal(totalFeesDeposited); // Check that rewards are claimable for both relayers const relayer1ClaimableRewards = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); const relayer2ClaimableRewards = await relayerRewardsPool.claimableRewards(relayer2.address, roundId); // Both relayers should have equal claimable rewards since they performed the same weighted actions (0, chai_1.expect)(relayer1ClaimableRewards).to.be.gt(0n); (0, chai_1.expect)(relayer2ClaimableRewards).to.be.gt(0n); (0, chai_1.expect)(relayer1ClaimableRewards).to.equal(relayer2ClaimableRewards); // Check initial relayer balances const initialRelayer1Balance = await b3tr.balanceOf(relayer1.address); const initialRelayer2Balance = await b3tr.balanceOf(relayer2.address); // Now relayers claim their own rewards from the pool const claimTx1 = await relayerRewardsPool.claimRewards(roundId, relayer1.address); await (0, chai_1.expect)(claimTx1) .to.emit(relayerRewardsPool, "RelayerRewardsClaimed") .withArgs(relayer1.address, roundId, relayer1ClaimableRewards); const claimTx2 = await relayerRewardsPool.claimRewards(roundId, relayer2.address); await (0, chai_1.expect)(claimTx2) .to.emit(relayerRewardsPool, "RelayerRewardsClaimed") .withArgs(relayer2.address, roundId, relayer2ClaimableRewards); // Check final balances - relayers should have received their rewards const finalRelayer1Balance = await b3tr.balanceOf(relayer1.address); const finalRelayer2Balance = await b3tr.balanceOf(relayer2.address); (0, chai_1.expect)(finalRelayer1Balance - initialRelayer1Balance).to.equal(relayer1ClaimableRewards); (0, chai_1.expect)(finalRelayer2Balance - initialRelayer2Balance).to.equal(relayer2ClaimableRewards); (0, chai_1.expect)(await relayerRewardsPool.claimableRewards(relayer1.address, roundId)).to.equal(0n); (0, chai_1.expect)(await relayerRewardsPool.claimableRewards(relayer2.address, roundId)).to.equal(0n); // Verify relayers cannot claim again await (0, chai_1.expect)(relayerRewardsPool.claimRewards(roundId, relayer1.address)).to.be.revertedWithCustomError(relayerRewardsPool, "RewardsAlreadyClaimed"); await (0, chai_1.expect)(relayerRewardsPool.claimRewards(roundId, relayer2.address)).to.be.revertedWithCustomError(relayerRewardsPool, "RewardsAlreadyClaimed"); }); (0, mocha_1.it)("should correctly apply fee cap when calculated fee exceeds cap", async function () { // This test verifies that the fee cap (100 B3TR) is correctly applied // when the calculated fee (10% of reward) would exceed the cap const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(owner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); // Wait for next cycle and start emissions await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Enable auto-voting for user await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Start a new round await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // Auto-vote for user await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); await (0, helpers_1.waitForRoundToEnd)(roundId); // Get net rewards and fee from contract const netUserReward = await voterRewards.getReward(roundId, user.address); const netUserGMReward = await voterRewards.getGMReward(roundId, user.address); const actualFee = await voterRewards.getRelayerFee(roundId, user.address); // Calculate raw total reward (before fees) const rawTotalReward = netUserReward + netUserGMReward + actualFee; const feePercentage = await relayerRewardsPool.getRelayerFeePercentage(); const feeDenominator = await relayerRewardsPool.getRelayerFeeDenominator(); const feeCap = await relayerRewardsPool.getFeeCap(); // Calculate what the fee would be without cap applied to RAW total const calculatedFeeWithoutCap = (rawTotalReward * feePercentage) / feeDenominator; // Verify fee cap logic if (calculatedFeeWithoutCap > feeCap) { (0, chai_1.expect)(actualFee).to.equal(feeCap, "Fee should be capped at maximum fee cap"); (0, chai_1.expect)(actualFee < calculatedFeeWithoutCap).to.be.true; } else { (0, chai_1.expect)(actualFee).to.equal(calculatedFeeWithoutCap, "Fee should equal calculated fee when under cap"); (0, chai_1.expect)(actualFee).to.equal((rawTotalReward * 10n) / 100n, "Fee should be exactly 10% of raw total"); } // Verify the mathematical relationship (0, chai_1.expect)(calculatedFeeWithoutCap).to.equal((rawTotalReward * 10n) / 100n, "Calculated fee should be 10% of RAW total reward"); // Verify fee parameters are as expected (0, chai_1.expect)(feePercentage).to.equal(10n, "Fee percentage should be 10"); (0, chai_1.expect)(feeDenominator).to.equal(100n, "Fee denominator should be 100"); (0, chai_1.expect)(feeCap).to.equal(hardhat_1.ethers.parseEther("100"), "Fee cap should be 100 B3TR"); // Verify that net + fee = raw (accounting for proportional distribution) (0, chai_1.expect)(netUserReward + netUserGMReward + actualFee).to.equal(rawTotalReward, "Net rewards plus fee should equal raw total"); }); (0, mocha_1.it)("[Edge Case] should handle when one user becomes non-person during auto-voting round", async function () { // This test verifies that when a user loses their personhood status (gets blacklisted, etc) // during an auto-voting round, the system properly handles the dynamic reduction of // expected actions. It ensures that: // // 1. Auto-voting is disabled for the blacklisted user // 2. Expected total actions are reduced appropriately // 3. Relayer rewards remain claimable for successful actions // 4. The round can still complete successfully with reduced participation // // This prevents scenarios where blacklisted users could block relayer rewards // by being counted in expected actions but unable to actually vote. // Setup additional relayer const relayer2 = otherAccounts[5]; await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(owner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Enable auto-voting for both users await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await xAllocationVoting.connect(user1).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user1).toggleAutoVoting(user1.address); // Start a new round - both users should be counted in total actions await (0, helpers_1.waitForNextCycle)(emissions); const tx = await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // Verify that expected actions are set for 2 auto-voting users (2 users * 2 actions each = 4 total) const voteWeight = await relayerRewardsPool.getVoteWeight(); const claimWeight = await relayerRewardsPool.getClaimWeight(); const expectedTotalActions = 4; // 2 users * 2 actions each const expectedTotalWeightedActions = 2n * (voteWeight + claimWeight); // 2 users * (vote + claim weights) await (0, chai_1.expect)(tx) .to.emit(relayerRewardsPool, "TotalAutoVotingActionsSet") .withArgs(roundId, 2, expectedTotalActions, expectedTotalWeightedActions, 2); // 2 relayers registered (0, chai_1.expect)(await relayerRewardsPool.totalActions(roundId)).to.equal(expectedTotalActions); (0, chai_1.expect)(await relayerRewardsPool.totalWeightedActions(roundId)).to.equal(expectedTotalWeightedActions); // Successfully cast vote for first user await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); // Un-whitelist user1 to make them non-person await veBetterPassport.connect(owner).blacklist(user1.address); // Verify user1 is no longer a person const [isPerson] = await veBetterPassport.isPersonAtTimepoint(user1.address, await xAllocationVoting.clock()); (0, chai_1.expect)(isPerson).to.be.false; // Calculate expected values after reduction const reducedTotalActions = expectedTotalActions - 2; // Remove 2 actions for 1 user const reducedTotalWeightedActions = expectedTotalWeightedActions - (voteWeight + claimWeight); // Remove weighted actions for 1 user // Attempt to cast vote for user1 should toggle off auto-voting and reduce expected actions const castVoteTx = xAllocationVoting.connect(relayer2).castVoteOnBehalfOf(user1.address, roundId); await (0, chai_1.expect)(castVoteTx).to.emit(xAllocationVoting, "AutoVotingToggled").withArgs(user1.address, false); await (0, chai_1.expect)(castVoteTx) .to.emit(xAllocationVoting, "AutoVoteSkipped") .withArgs(user1.address, roundId, false, 1, await vot3.balanceOf(user1.address)); await (0, chai_1.expect)(castVoteTx).to.not.emit(xAllocationVoting, "AllocationVoteCast"); // Check that expected actions reduction event is emitted await (0, chai_1.expect)(castVoteTx) .to.emit(relayerRewardsPool, "ExpectedActionsReduced") .withArgs(roundId, 2, reducedTotalActions, reducedTotalWeightedActions); (0, chai_1.expect)(await relayerRewardsPool.totalActions(roundId)).to.equal(reducedTotalActions); (0, chai_1.expect)(await relayerRewardsPool.totalWeightedActions(roundId)).to.equal(reducedTotalWeightedActions); // Verify user1 auto-voting is now disabled and app preferences were cleared (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledAtTimepoint(user1.address, await xAllocationVoting.clock())) .to.be.false; (0, chai_1.expect)(await xAllocationVoting.getUserVotingPreferences(user1.address)).to.deep.equal([]); await (0, helpers_1.waitForRoundToEnd)(roundId); // Only relayer1 should be able to claim rewards for user (who successfully voted) const userReward = await voterRewards.getReward(roundId, user.address); const userFee = await voterRewards.getRelayerFee(roundId, user.address); (0, chai_1.expect)(userReward).to.be.gt(0); (0, chai_1.expect)(userFee).to.be.gt(0); // Relayer1 claims for user await voterRewards.connect(relayer1).claimReward(roundId, user.address); // Verify that with reduced expected actions, relayer rewards are still claimable // Even though only 1 user voted instead of 2, the round is still valid (0, chai_1.expect)(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; // Check that completed weighted actions match the reduced total weighted actions const completedWeightedActions = await relayerRewardsPool.completedWeightedActions(roundId); (0, chai_1.expect)(completedWeightedActions).to.equal(reducedTotalWeightedActions); // Relayer1 should be able to claim their proportional rewards const relayer1ClaimableRewards = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); (0, chai_1.expect)(relayer1ClaimableRewards).to.be.gt(0); await relayerRewardsPool.claimRewards(roundId, relayer1.address); // Verify relayer2 gets no rewards since they performed no successful actions (0, chai_1.expect)(await relayerRewardsPool.claimableRewards(relayer2.address, roundId)).to.equal(0); }); (0, mocha_1.it)("[Race Condition] should handle when 2 relayers try to vote for same user in same block", async function () { // This test verifies that when multiple relayers attempt to vote for the same user // in the same block, only one vote succeeds and only that relayer gets credit. // This prevents double-voting and ensures fair relayer reward distribution. // Setup additional relayer const relayer2 = otherAccounts[5]; await relayerRewardsPool.connect(owner).registerRelayer(relayer2.address); const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(owner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Enable auto-voting for user await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Start a new round await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); const voteWeight = await relayerRewardsPool.getVoteWeight(); // First relayer successfully votes const tx1 = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); await (0, chai_1.expect)(tx1) .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user.address, roundId, 1, voteWeight); // Verify the vote was cast await (0, chai_1.expect)(tx1).to.emit(xAllocationVoting, "AllocationVoteCast"); // Second relayer tries to vote for the same user - should fail await (0, chai_1.expect)(xAllocationVoting.connect(relayer2).castVoteOnBehalfOf(user.address, roundId)).to.be.revertedWithCustomError(xAllocationVoting, "VoteAlreadyProcessed"); // Verify only relayer1 got credit for the vote const relayer1Actions = await relayerRewardsPool.totalRelayerActions(relayer1.address, roundId); const relayer2Actions = await relayerRewardsPool.totalRelayerActions(relayer2.address, roundId); (0, chai_1.expect)(relayer1Actions).to.equal(1); (0, chai_1.expect)(relayer2Actions).to.equal(0); // Wait for round to end and claim rewards await (0, helpers_1.waitForRoundToEnd)(roundId); const userReward = await voterRewards.getReward(roundId, user.address); const userFee = await voterRewards.getRelayerFee(roundId, user.address); (0, chai_1.expect)(userReward).to.be.gt(0); (0, chai_1.expect)(userFee).to.be.gt(0); // Both relayers try to claim - first one succeeds const claimWeight = await relayerRewardsPool.getClaimWeight(); const claimTx = await voterRewards.connect(relayer1).claimReward(roundId, user.address); await (0, chai_1.expect)(claimTx) .to.emit(relayerRewardsPool, "RelayerActionRegistered") .withArgs(relayer1.address, user.address, roundId, 2, claimWeight); // Second relayer tries to claim for same user - should fail await (0, chai_1.expect)(voterRewards.connect(relayer2).claimReward(roundId, user.address)).to.be.revertedWith("VoterRewards: reward must be greater than 0"); // Verify final relayer action counts const finalRelayer1Actions = await relayerRewardsPool.totalRelayerActions(relayer1.address, roundId); const finalRelayer2Actions = await relayerRewardsPool.totalRelayerActions(relayer2.address, roundId); (0, chai_1.expect)(finalRelayer1Actions).to.equal(2); // Vote + Claim (0, chai_1.expect)(finalRelayer2Actions).to.equal(0); // No successful actions // Verify relayer rewards distribution const relayer1ClaimableRewards = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); const relayer2ClaimableRewards = await relayerRewardsPool.claimableRewards(relayer2.address, roundId); (0, chai_1.expect)(relayer1ClaimableRewards).to.be.gt(0); // Relayer1 should get all rewards (0, chai_1.expect)(relayer2ClaimableRewards).to.equal(0); // Relayer2 should get nothing }); }); });