UNPKG

@vechain/vebetterdao-contracts

Version:

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

659 lines (658 loc) 42.9 kB
import { ethers } from "hardhat"; import { expect } from "chai"; import { describe, it, beforeEach } from "mocha"; import { getOrDeployContractInstances } from "../helpers/deploy"; import { bootstrapAndStartEmissions, getVot3Tokens, waitForRoundToEnd, waitForNextBlock, createProposal, payDeposit, waitForProposalToBeActive, getProposalIdFromTx, moveBlocks, } from "../helpers/common"; import { endorseApp } from "../helpers/xnodes"; describe("Relayer + Navigator E2E - @shard20e2e", function () { let navigatorRegistry; let xAllocationVoting; let governor; let voterRewards; let b3tr; let vot3; let emissions; let veBetterPassport; let x2EarnApps; let relayerRewardsPool; let owner; let minterAccount; let otherAccounts; let creators; let nav1; let nav2; let citizenA; let citizenB; let citizenC; let autoUser1; let autoUser2; let relayer1; let relayer2; let app1Id; let app2Id; const STAKE = ethers.parseEther("50000"); const DELEGATE_AMT = ethers.parseEther("500"); const CITIZEN_VOT3 = "1000"; const AUTO_VOT3 = "100"; const fundAndApprove = async (acct, amount) => { await b3tr.connect(owner).transfer(acct.address, amount); await b3tr.connect(acct).approve(await navigatorRegistry.getAddress(), amount); }; const advanceRound = async () => { const cur = await xAllocationVoting.currentRoundId(); await waitForRoundToEnd(Number(cur)); await emissions.distribute(); return xAllocationVoting.currentRoundId(); }; const createActiveProposal = async (description, targetRound) => { await getVot3Tokens(owner, "300000"); await waitForNextBlock(); const tx = await createProposal(b3tr, await ethers.getContractFactory("B3TR"), owner, description, "tokenDetails", [], targetRound.toString()); const proposalId = await getProposalIdFromTx(tx); await payDeposit(proposalId.toString(), owner); return proposalId; }; async function advancePastSkipWindow(roundId) { const skipWindow = await xAllocationVoting.citizenSkipWindowBlocks(); const deadline = await xAllocationVoting.roundDeadline(roundId); const currentBlock = BigInt(await ethers.provider.getBlockNumber()); const blocksToMine = deadline - currentBlock - skipWindow; if (blocksToMine > 0n) await moveBlocks(Number(blocksToMine)); } /** * Deploy all contracts, create apps, register navigators, delegate citizens, * register relayers, bootstrap emissions. Auto-voting is NOT enabled by default * to avoid governance action overcounting — tests that need it call enableAutoVoting(). */ async function setupFullEcosystem() { const d = await getOrDeployContractInstances({ forceDeploy: true }); if (!d) throw new Error("deploy failed"); navigatorRegistry = d.navigatorRegistry; xAllocationVoting = d.xAllocationVoting; governor = d.governor; voterRewards = d.voterRewards; b3tr = d.b3tr; vot3 = d.vot3; emissions = d.emissions; veBetterPassport = d.veBetterPassport; x2EarnApps = d.x2EarnApps; relayerRewardsPool = d.relayerRewardsPool; owner = d.owner; minterAccount = d.minterAccount; otherAccounts = d.otherAccounts; creators = d.creators; nav1 = otherAccounts[8]; nav2 = otherAccounts[9]; citizenA = otherAccounts[10]; citizenB = otherAccounts[11]; citizenC = otherAccounts[12]; autoUser1 = otherAccounts[13]; autoUser2 = otherAccounts[14]; relayer1 = otherAccounts[15]; relayer2 = otherAccounts[3]; await b3tr.connect(minterAccount).mint(owner.address, ethers.parseEther("10000000")); await getVot3Tokens(owner, "10000000"); // Apps await x2EarnApps.connect(creators[0]).submitApp(creators[0].address, creators[0].address, "E2EApp1", "uri"); app1Id = await x2EarnApps.hashAppName("E2EApp1"); await endorseApp(app1Id, otherAccounts[3]); await x2EarnApps.connect(creators[1]).submitApp(creators[1].address, creators[1].address, "E2EApp2", "uri"); app2Id = await x2EarnApps.hashAppName("E2EApp2"); await endorseApp(app2Id, otherAccounts[4]); // Register navigators await fundAndApprove(nav1, STAKE); await navigatorRegistry.connect(nav1).register(STAKE, "ipfs://nav1"); await fundAndApprove(nav2, STAKE); await navigatorRegistry.connect(nav2).register(STAKE, "ipfs://nav2"); // Passport whitelist if (!(await veBetterPassport.isCheckEnabled(1))) await veBetterPassport.toggleCheck(1); for (const acct of [citizenA, citizenB, citizenC, autoUser1, autoUser2]) { await veBetterPassport.whitelist(acct.address); } // VOT3 for citizens for (const acct of [citizenA, citizenB, citizenC]) { await getVot3Tokens(acct, CITIZEN_VOT3); } // VOT3 for auto-voting users for (const acct of [autoUser1, autoUser2]) { await getVot3Tokens(acct, AUTO_VOT3); } // Register relayers await relayerRewardsPool.registerRelayer(relayer1.address); await relayerRewardsPool.registerRelayer(relayer2.address); await relayerRewardsPool.connect(owner).setRelayerFeePercentage(10); await bootstrapAndStartEmissions(); await waitForNextBlock(); // Delegate citizens await navigatorRegistry.connect(citizenA).delegate(nav1.address, DELEGATE_AMT); await navigatorRegistry.connect(citizenB).delegate(nav2.address, DELEGATE_AMT); await navigatorRegistry.connect(citizenC).delegate(nav1.address, DELEGATE_AMT); await waitForNextBlock(); } /** Enable auto-voting for autoUser1 and autoUser2. Call before the target round starts. */ const enableAutoVoting = async () => { await xAllocationVoting.connect(autoUser1).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(autoUser1).toggleAutoVoting(autoUser1.address); await xAllocationVoting.connect(autoUser2).setUserVotingPreferences([app2Id]); await xAllocationVoting.connect(autoUser2).toggleAutoVoting(autoUser2.address); }; // ==================================================================== // 1. Happy path: all navigators set all choices (with governance) // ==================================================================== describe("1 - Happy path: navigators set all choices, relayer completes everything", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("relayer votes allocation + governance for citizens, claims rewards", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId = await createActiveProposal("happy-path-proposal", targetRound); const roundId = await advanceRound(); expect(roundId).to.equal(targetRound); await waitForProposalToBeActive(proposalId); // Nav1 sets both allocation and governance await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [6000, 4000]); await navigatorRegistry.connect(nav1).setProposalDecision(proposalId, 2); // Nav2 sets both await navigatorRegistry.connect(nav2).setAllocationPreferences(roundId, [app1Id, app2Id], [5000, 5000]); await navigatorRegistry.connect(nav2).setProposalDecision(proposalId, 2); // Navigator allocation votes await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); // Navigator governance votes await governor.connect(relayer1).castNavigatorVote(proposalId, citizenA.address); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenB.address); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenC.address); expect(await xAllocationVoting.hasVoted(roundId, citizenA.address)).to.be.true; expect(await xAllocationVoting.hasVoted(roundId, citizenB.address)).to.be.true; expect(await governor.hasVoted(proposalId, citizenA.address)).to.be.true; // End round, voter claims (registers CLAIM actions), pool claim await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenB.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 2. Navigator sets allocation but NO governance decision // ==================================================================== describe("2 - Allocation prefs set, no governance decision", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("governance skips, claim auto-reduced, relayer claims rewards", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId = await createActiveProposal("alloc-no-gov", targetRound); const roundId = await advanceRound(); await waitForProposalToBeActive(proposalId); // Nav1 sets allocation ONLY await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [6000, 4000]); // Nav2 also no governance decision await navigatorRegistry.connect(nav2).setAllocationPreferences(roundId, [app1Id, app2Id], [5000, 5000]); // Advance past skip window so governance skips are permitted await advancePastSkipWindow(roundId); // Allocation vote succeeds await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); expect(await xAllocationVoting.hasVoted(roundId, citizenA.address)).to.be.true; // Governance vote skips (no decision set) await expect(governor.connect(relayer1).castNavigatorVote(proposalId, citizenA.address)).to.not.be.reverted; // citizenA: allocation voted (not reduced), governance reduced. // Claim NOT auto-reduced because allocation was cast (userAllocationVoteReduced = false). // Relayer must still claim voter rewards for citizenA. // Handle other citizens similarly await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenC.address); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenB.address); await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenB.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 3. Navigator sets governance decision but NO allocation prefs // Allocation skip reduces vote expectation. Governance vote succeeds and // registers voter rewards (both allocation AND governance votes earn rewards). // Claim is NOT auto-reduced (_checkAndReduceClaim requires userGovernanceVoteReduced // which is only set on skip, not on successful vote). But the citizen earned // governance-based rewards, so claimReward succeeds and registers the CLAIM action. // ==================================================================== describe("3 - Governance decision set, no allocation prefs", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("allocation skips, governance votes, citizen still earns rewards via governance, relayer claims", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId = await createActiveProposal("gov-no-alloc", targetRound); const roundId = await advanceRound(); await waitForProposalToBeActive(proposalId); // Nav1 sets governance ONLY await navigatorRegistry.connect(nav1).setProposalDecision(proposalId, 2); // Advance past skip window so allocation skips are permitted await advancePastSkipWindow(roundId); // Allocation vote skips (no prefs) await expect(xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId)).to.emit(xAllocationVoting, "NavigatorVoteSkipped"); await expect(xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId)).to.emit(xAllocationVoting, "NavigatorVoteSkipped"); // Governance vote succeeds — also registers voter rewards via voterRewards.registerVote await governor.connect(relayer1).castNavigatorVote(proposalId, citizenA.address); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenC.address); expect(await governor.hasVoted(proposalId, citizenA.address)).to.be.true; // citizenB (nav2 no choices → all skips → claim auto-reduced) await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenB.address); await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); // Citizens earned rewards from governance votes — claimReward succeeds await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 4. Two governance proposals, navigator sets decision for only one // ==================================================================== describe("4 - Two proposals, decision for only one", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("partial governance skip, relayer claims rewards", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId1 = await createActiveProposal("partial-gov-1", targetRound); const proposalId2 = await createActiveProposal("partial-gov-2", targetRound); const roundId = await advanceRound(); await waitForProposalToBeActive(proposalId1); await waitForProposalToBeActive(proposalId2); expect(await governor.getActiveProposals()).to.deep.equal([proposalId1, proposalId2]); // Nav1 sets allocation + decision for proposal1 only await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [6000, 4000]); await navigatorRegistry.connect(nav1).setProposalDecision(proposalId1, 2); // Advance past skip window so skips are permitted await advancePastSkipWindow(roundId); // citizenA: allocation succeeds, gov proposal1 succeeds, gov proposal2 skips await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId1, citizenA.address); await governor.connect(relayer1).castNavigatorVote(proposalId2, citizenA.address); expect(await governor.hasVoted(proposalId1, citizenA.address)).to.be.true; // citizenC same await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId1, citizenC.address); await governor.connect(relayer1).castNavigatorVote(proposalId2, citizenC.address); // citizenB (nav2 no choices) — skip all await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId1, citizenB.address); await governor.connect(relayer1).castNavigatorVote(proposalId2, citizenB.address); // Claim NOT auto-reduced for citizenA/C (allocation was cast → userAllocationVoteReduced = false) await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); // citizenB was fully auto-reduced (all skips) expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 5. Navigator sets no choices at all // ==================================================================== describe("5 - Navigator sets no choices at all", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("all skips, all reductions, relayer claims rewards", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId = await createActiveProposal("no-choices", targetRound); const roundId = await advanceRound(); await waitForProposalToBeActive(proposalId); const totalBefore = await relayerRewardsPool.totalActions(roundId); // Nav1 + Nav2 set NOTHING // Advance past skip window so skips are permitted await advancePastSkipWindow(roundId); // Skip all for every citizen for (const citizen of [citizenA, citizenB, citizenC]) { await xAllocationVoting.connect(relayer1).castNavigatorVote(citizen.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizen.address); } // 3 actions per citizen (alloc + gov + claim) × 3 citizens = 9 reductions expect(await relayerRewardsPool.totalActions(roundId)).to.equal(totalBefore - 9n); await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); // No voter claims needed — all auto-reduced expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; }); }); // ==================================================================== // 6. Dead navigator — immediate skips // ==================================================================== describe("6 - Dead navigator: immediate skips", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("dead nav skips immediately, system does not stale, relayer claims rewards", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId = await createActiveProposal("dead-nav", targetRound); const roundId = await advanceRound(); await waitForProposalToBeActive(proposalId); // Kill nav2 after round starts await navigatorRegistry.connect(owner).deactivateNavigator(nav2.address, 0, false); const totalBefore = await relayerRewardsPool.totalActions(roundId); // citizenB: dead nav → immediate skip for alloc + gov → claim auto-reduced await expect(xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId)).to.emit(xAllocationVoting, "NavigatorVoteSkipped"); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenB.address); expect(await relayerRewardsPool.totalActions(roundId)).to.equal(totalBefore - 3n); // Nav1 alive — set choices and vote await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [5000, 5000]); await navigatorRegistry.connect(nav1).setProposalDecision(proposalId, 2); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenA.address); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenC.address); await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 7. Mixed: auto-voting users + citizens (no governance proposals) // ==================================================================== describe("7 - Mixed: auto-voting users + citizens with cooperating/non-cooperating navigators", function () { beforeEach(async function () { await setupFullEcosystem(); await enableAutoVoting(); }); it("exact action accounting, rewards claimable", async function () { // No governance proposals → no governance action overcounting for auto-users const roundId = await advanceRound(); const voteWeight = await relayerRewardsPool.getVoteWeight(); const claimWeight = await relayerRewardsPool.getClaimWeight(); // allocationUsers = 2 auto + 3 citizens = 5 // No governance → total = 5 × 2 = 10 const totalActions = await relayerRewardsPool.totalActions(roundId); expect(totalActions).to.equal(10n); expect(await relayerRewardsPool.totalWeightedActions(roundId)).to.equal(5n * (voteWeight + claimWeight)); // Nav1 sets allocation prefs await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [6000, 4000]); // Nav2 sets nothing // Auto-votes await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(autoUser1.address, roundId); await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(autoUser2.address, roundId); // Advance past skip window so skips are permitted await advancePastSkipWindow(roundId); // Nav1's citizens: vote await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); // Nav2's citizen: skip → claim auto-reduced (no gov proposals → only alloc needs reducing) await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId); await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); // Voter claims await voterRewards.connect(relayer1).claimReward(roundId, autoUser1.address); await voterRewards.connect(relayer1).claimReward(roundId, autoUser2.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); const totalW = await relayerRewardsPool.totalWeightedActions(roundId); const completedW = await relayerRewardsPool.completedWeightedActions(roundId); expect(completedW).to.be.gte(totalW); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 8. Two relayers splitting work (no governance) // ==================================================================== describe("8 - Two relayers splitting work", function () { beforeEach(async function () { await setupFullEcosystem(); await enableAutoVoting(); }); it("rewards split proportionally between relayers", async function () { const roundId = await advanceRound(); // Nav1 sets allocation await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [5000, 5000]); // Relayer1 handles autoUser1 await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(autoUser1.address, roundId); // Relayer2 handles autoUser2 await xAllocationVoting.connect(relayer2).castVoteOnBehalfOf(autoUser2.address, roundId); // Advance past skip window so skips are permitted await advancePastSkipWindow(roundId); // Relayer1: citizenA + citizenC (nav1 has prefs → votes) await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); // Relayer2: citizenB (nav2 no prefs → skip) await xAllocationVoting.connect(relayer2).castNavigatorVote(citizenB.address, roundId); await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); // Voter claims split by relayer await voterRewards.connect(relayer1).claimReward(roundId, autoUser1.address); await voterRewards.connect(relayer2).claimReward(roundId, autoUser2.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); const r1Weighted = await relayerRewardsPool.totalRelayerWeightedActions(relayer1.address, roundId); const r2Weighted = await relayerRewardsPool.totalRelayerWeightedActions(relayer2.address, roundId); expect(r1Weighted).to.be.gt(0n); expect(r2Weighted).to.be.gt(0n); expect(r1Weighted).to.be.gt(r2Weighted); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const claim1 = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); const claim2 = await relayerRewardsPool.claimableRewards(relayer2.address, roundId); expect(claim1).to.be.gt(claim2); expect(claim1).to.be.gt(0n); expect(claim2).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); await relayerRewardsPool.connect(relayer2).claimRewards(roundId, relayer2.address); }); }); // ==================================================================== // 9. No auto-voting users, only navigator citizens // ==================================================================== describe("9 - Only navigator citizens, no auto-voting users", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("system works with citizens only, no governance", async function () { const roundId = await advanceRound(); // 3 citizens × 2 (vote+claim) = 6 total actions expect(await relayerRewardsPool.totalActions(roundId)).to.equal(6n); await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [5000, 5000]); await navigatorRegistry.connect(nav2).setAllocationPreferences(roundId, [app1Id, app2Id], [5000, 5000]); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenB.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 10. Navigator alive at snapshot, dies mid-round // ==================================================================== describe("10 - Navigator alive at snapshot, dies mid-round", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("mid-round death causes skip, reductions restore claimability", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId = await createActiveProposal("mid-death", targetRound); const roundId = await advanceRound(); await waitForProposalToBeActive(proposalId); const totalBefore = await relayerRewardsPool.totalActions(roundId); // Kill nav2 mid-round await navigatorRegistry.connect(owner).deactivateNavigator(nav2.address, 0, false); // citizenB: dead nav → skip alloc + gov → claim auto-reduced await expect(xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId)).to.emit(xAllocationVoting, "NavigatorVoteSkipped"); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenB.address); expect(await relayerRewardsPool.totalActions(roundId)).to.equal(totalBefore - 3n); // Nav1 alive await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [5000, 5000]); await navigatorRegistry.connect(nav1).setProposalDecision(proposalId, 2); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenA.address); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenC.address); await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); const totalW = await relayerRewardsPool.totalWeightedActions(roundId); const completedW = await relayerRewardsPool.completedWeightedActions(roundId); expect(completedW).to.be.gte(totalW); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 11. Auto-voting users + governance proposal (no stale) // Governance actions are only counted for citizens (governanceUsers), // not auto-voting users. This test combines both in the same round. // ==================================================================== describe("11 - Auto-voting users + governance proposal, pool does not stale", function () { beforeEach(async function () { await setupFullEcosystem(); await enableAutoVoting(); }); it("governance actions counted only for citizens, isRewardClaimable true after all work", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId = await createActiveProposal("auto-gov", targetRound); const roundId = await advanceRound(); await waitForProposalToBeActive(proposalId); const voteWeight = await relayerRewardsPool.getVoteWeight(); const claimWeight = await relayerRewardsPool.getClaimWeight(); // allocationUsers = 2 auto + 3 citizens = 5 → allocation actions = 5 * 2 = 10 // governanceUsers = 3 citizens only → governance actions = 1 * 3 = 3 // total = 10 + 3 = 13 const totalActions = await relayerRewardsPool.totalActions(roundId); expect(totalActions).to.equal(13n); const expectedWeighted = 5n * (voteWeight + claimWeight) + 3n * voteWeight; expect(await relayerRewardsPool.totalWeightedActions(roundId)).to.equal(expectedWeighted); // Nav1 sets all choices await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [6000, 4000]); await navigatorRegistry.connect(nav1).setProposalDecision(proposalId, 2); // Nav2 sets nothing → citizenB skips // Auto-votes await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(autoUser1.address, roundId); await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(autoUser2.address, roundId); // Advance past skip window so skips are permitted await advancePastSkipWindow(roundId); // Nav1's citizens: alloc + gov await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenC.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenA.address); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenC.address); // Nav2's citizen: alloc skip + gov skip → claim auto-reduced await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenB.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenB.address); // End round, voter claims await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); await voterRewards.connect(relayer1).claimReward(roundId, autoUser1.address); await voterRewards.connect(relayer1).claimReward(roundId, autoUser2.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer1).claimReward(roundId, citizenC.address); const totalW = await relayerRewardsPool.totalWeightedActions(roundId); const completedW = await relayerRewardsPool.completedWeightedActions(roundId); expect(completedW).to.be.gte(totalW); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; const reward = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); expect(reward).to.be.gt(0n); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); }); }); // ==================================================================== // 12. Two relayers with governance, verify proportional reward math // ==================================================================== describe("12 - Two relayers with governance, proportional rewards", function () { beforeEach(async function () { await setupFullEcosystem(); }); it("rewards split proportionally, claim1 + claim2 = totalRewards (minus rounding)", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const targetRound = currentRound + 1n; const proposalId = await createActiveProposal("two-relayer-gov", targetRound); const roundId = await advanceRound(); await waitForProposalToBeActive(proposalId); const voteWeight = await relayerRewardsPool.getVoteWeight(); const claimWeight = await relayerRewardsPool.getClaimWeight(); // Both navigators set all choices await navigatorRegistry.connect(nav1).setAllocationPreferences(roundId, [app1Id, app2Id], [6000, 4000]); await navigatorRegistry.connect(nav1).setProposalDecision(proposalId, 2); await navigatorRegistry.connect(nav2).setAllocationPreferences(roundId, [app1Id, app2Id], [5000, 5000]); await navigatorRegistry.connect(nav2).setProposalDecision(proposalId, 2); // Relayer1: citizenA (alloc + gov) await xAllocationVoting.connect(relayer1).castNavigatorVote(citizenA.address, roundId); await governor.connect(relayer1).castNavigatorVote(proposalId, citizenA.address); // Relayer2: citizenB (alloc + gov) + citizenC (alloc + gov) await xAllocationVoting.connect(relayer2).castNavigatorVote(citizenB.address, roundId); await xAllocationVoting.connect(relayer2).castNavigatorVote(citizenC.address, roundId); await governor.connect(relayer2).castNavigatorVote(proposalId, citizenB.address); await governor.connect(relayer2).castNavigatorVote(proposalId, citizenC.address); // End round, voter claims split by relayer await waitForRoundToEnd(Number(roundId)); await emissions.distribute(); await voterRewards.connect(relayer1).claimReward(roundId, citizenA.address); await voterRewards.connect(relayer2).claimReward(roundId, citizenB.address); await voterRewards.connect(relayer2).claimReward(roundId, citizenC.address); expect(await relayerRewardsPool.isRewardClaimable(roundId)).to.be.true; // Verify weighted actions: each citizen = voteWeight (alloc) + voteWeight (gov) + claimWeight const perCitizen = voteWeight * 2n + claimWeight; const r1Weighted = await relayerRewardsPool.totalRelayerWeightedActions(relayer1.address, roundId); const r2Weighted = await relayerRewardsPool.totalRelayerWeightedActions(relayer2.address, roundId); expect(r1Weighted).to.equal(perCitizen); // 1 citizen expect(r2Weighted).to.equal(perCitizen * 2n); // 2 citizens const totalRewards = await relayerRewardsPool.getTotalRewards(roundId); const claim1 = await relayerRewardsPool.claimableRewards(relayer1.address, roundId); const claim2 = await relayerRewardsPool.claimableRewards(relayer2.address, roundId); // Proportional: relayer2 did 2x the work of relayer1 expect(claim2).to.equal(claim1 * 2n); // claim1 + claim2 = totalRewards (exact, no rounding dust with these weights) expect(claim1 + claim2).to.equal(totalRewards); // Both claim successfully const bal1Before = await b3tr.balanceOf(relayer1.address); const bal2Before = await b3tr.balanceOf(relayer2.address); await relayerRewardsPool.connect(relayer1).claimRewards(roundId, relayer1.address); await relayerRewardsPool.connect(relayer2).claimRewards(roundId, relayer2.address); expect(await b3tr.balanceOf(relayer1.address)).to.equal(bal1Before + claim1); expect(await b3tr.balanceOf(relayer2.address)).to.equal(bal2Before + claim2); }); }); });