UNPKG

@vechain/vebetterdao-contracts

Version:

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

706 lines (705 loc) 85 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const hardhat_1 = require("hardhat"); const mocha_1 = require("mocha"); const chai_1 = require("chai"); const helpers_1 = require("../../helpers"); const xnodes_1 = require("../../helpers/xnodes"); (0, mocha_1.describe)("AutoVoting - @shard14b", function () { let xAllocationVoting; let x2EarnApps; let veBetterPassport; let voterRewards; let b3tr; let emissions; let relayerRewardsPool; let vot3; let owner; let relayer1; let minterAccount; let otherAccounts; let user; let user1; let user2; let appOwner; let appOwner1; let appOwner2; let appOwner3; let x2EarnCreatorContract; let stargateNftMock; // 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; emissions = config.emissions; vot3 = config.vot3; owner = config.owner; minterAccount = config.minterAccount; otherAccounts = config.otherAccounts; relayerRewardsPool = config.relayerRewardsPool; relayer1 = otherAccounts[10]; user = otherAccounts[0]; user1 = otherAccounts[1]; user2 = otherAccounts[2]; appOwner = otherAccounts[11]; appOwner1 = otherAccounts[12]; appOwner2 = otherAccounts[13]; appOwner3 = otherAccounts[14]; x2EarnCreatorContract = config.x2EarnCreator; stargateNftMock = config.stargateNftMock; await b3tr.connect(owner).grantRole(await b3tr.MINTER_ROLE(), await emissions.getAddress()); await emissions.connect(minterAccount).bootstrap(); await relayerRewardsPool.connect(owner).setRelayerFeePercentage(10); 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, "100"); await (0, helpers_1.getVot3Tokens)(user1, "100"); await (0, helpers_1.getVot3Tokens)(user2, "100"); }; (0, mocha_1.describe)("Core logic", function () { (0, mocha_1.beforeEach)(async function () { await setupContracts(); await emissions.connect(minterAccount).start(); }); (0, mocha_1.it)("should toggle autovoting status correctly", async function () { const initialCycle = await emissions.getCurrentCycle(); const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(user.address)); await x2EarnApps.connect(owner).submitApp(user.address, user.address, user.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, user); await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); // Toggle auto-voting: ON, OFF, and ON await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.false; // Wait for the next cycle to be distributable await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // =========== cycle 1 =========== const cycle1 = await emissions.getCurrentCycle(); (0, chai_1.expect)(Number(cycle1)).to.be.greaterThan(Number(initialCycle)); await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); // The user must set his preferences again to be able to vote after autovoting is disabled (0, chai_1.expect)(await xAllocationVoting.getUserVotingPreferences(user.address)).to.deep.equal([app1Id]); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.true; }); (0, mocha_1.it)("should set and get user voting preferences", 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); // Set voting preferences first await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); // Enable autovoting await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Get and verify preferences const preferences = await xAllocationVoting.getUserVotingPreferences(user.address); (0, chai_1.expect)(preferences).to.deep.equal([app1Id]); }); (0, mocha_1.it)("revert when cast vote on behalf of user if autovoting is not enabled", async function () { 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 xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); const preferences = await xAllocationVoting.getUserVotingPreferences(user.address); (0, chai_1.expect)(preferences).to.deep.equal([app1Id]); await (0, helpers_1.startNewAllocationRound)(); // cast vote on behalf of user await (0, chai_1.expect)(xAllocationVoting.connect(owner).castVoteOnBehalfOf(user.address, 1)).to.be.revertedWithCustomError(xAllocationVoting, "AutoVotingNotEnabled"); }); (0, mocha_1.it)("should clear preferences when autovoting is disabled", async function () { 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); // Set preferences first then enable autovoting await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await (0, chai_1.expect)(xAllocationVoting.connect(user).toggleAutoVoting(user.address)) .to.emit(xAllocationVoting, "AutoVotingToggled") .withArgs(user.address, true); // Verify preferences are set let preferences = await xAllocationVoting.getUserVotingPreferences(user.address); (0, chai_1.expect)(preferences).to.deep.equal([app1Id]); // Disable autovoting const txn = await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await (0, chai_1.expect)(txn).to.emit(xAllocationVoting, "AutoVotingToggled").withArgs(user.address, false); // Preferences should be cleared preferences = await xAllocationVoting.getUserVotingPreferences(user.address); (0, chai_1.expect)(preferences).to.deep.equal([]); }); (0, mocha_1.it)("should validate app preferences", async function () { // Should revert with empty apps array await (0, chai_1.expect)(xAllocationVoting.connect(user).setUserVotingPreferences([])).to.be.revertedWith("AutoVotingLogic: no apps to vote for"); // Should revert with invalid app const invalidAppId = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("invalid")); await (0, chai_1.expect)(xAllocationVoting.connect(user).setUserVotingPreferences([invalidAppId])).to.be.revertedWith("AutoVotingLogic: invalid app"); }); (0, mocha_1.it)("should count total auto-voting users", async function () { const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(user.address)); await x2EarnApps.connect(owner).submitApp(user.address, user.address, user.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, user); await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user1).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user2).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await xAllocationVoting.connect(user1).toggleAutoVoting(user1.address); await xAllocationVoting.connect(user2).toggleAutoVoting(user2.address); // Still disabled until the next cycle (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.false; (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user1.address)).to.be.false; (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user2.address)).to.be.false; (0, chai_1.expect)(await xAllocationVoting.getTotalAutoVotingUsersAtRoundStart()).to.equal(0); // Wait for the next cycle to be distributable await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.true; (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user1.address)).to.be.true; (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user2.address)).to.be.true; (0, chai_1.expect)(await xAllocationVoting.getTotalAutoVotingUsersAtRoundStart()).to.equal(3); (0, chai_1.expect)(await relayerRewardsPool.getMissedAutoVotingUsersCount(await xAllocationVoting.currentRoundId())).to.equal(3); await xAllocationVoting.connect(user2).toggleAutoVoting(user2.address); // remaining 3 until the next cycle (0, chai_1.expect)(await xAllocationVoting.getTotalAutoVotingUsersAtRoundStart()).to.equal(3); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user2.address)).to.be.false; (0, chai_1.expect)(await xAllocationVoting.getTotalAutoVotingUsersAtRoundStart()).to.equal(2); }); (0, mocha_1.it)("should revert if app preferences are empty before enabling autovoting", async function () { const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(user.address)); await x2EarnApps.connect(owner).submitApp(user.address, user.address, user.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, user); await (0, chai_1.expect)(xAllocationVoting.connect(user).toggleAutoVoting(user.address)).to.be.revertedWith("AutoVotingLogic: must select at least one app"); }); (0, mocha_1.it)("should revert when user with auto-voting enabled tries to cast manual vote", async function () { 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 xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); const userBalance = await vot3.balanceOf(user.address); // User with auto-voting enabled tries to manually cast vote - should revert await (0, chai_1.expect)(xAllocationVoting.connect(user).castVote(roundId, [app1Id], [userBalance])).to.be.revertedWithCustomError(xAllocationVoting, "AutoVotingEnabled"); }); (0, mocha_1.it)("should correctly handle auto-voting status when disabled mid-cycle", async function () { // Scenario: // Auto voting status // Initial cycle - OFF // Cycle 1 - ON (enabled during previous cycle) // Cycle 2 - OFF (disabled during cycle 1) // Cycle 3 - OFF (remains disabled) // Get initial cycle data const initialCycle = await emissions.getCurrentCycle(); const initialEmissionBlock = await emissions.lastEmissionBlock(); // Create a test app and set preferences first const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(user.address)); await x2EarnApps.connect(owner).submitApp(user.address, user.address, user.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, user); await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); // Enable auto-voting mid-cycle await xAllocationVoting.connect(user).toggleAutoVoting(user.address); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.false; // Wait for the next cycle to be distributable await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // =========== cycle 1 =========== const cycle1 = await emissions.getCurrentCycle(); const cycle1EmissionBlock = await emissions.lastEmissionBlock(); (0, chai_1.expect)(Number(cycle1)).to.be.greaterThan(Number(initialCycle)); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.true; // User disables auto-voting mid-cycle await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Wait for the next cycle await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // =========== cycle 2 =========== const cycle2 = await emissions.getCurrentCycle(); const cycle2EmissionBlock = await emissions.lastEmissionBlock(); (0, chai_1.expect)(Number(cycle2)).to.be.greaterThan(Number(cycle1)); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.false; await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // =========== cycle 3 =========== const cycle3 = await emissions.getCurrentCycle(); const cycle3EmissionBlock = await emissions.lastEmissionBlock(); // Verify cycle has advanced (0, chai_1.expect)(Number(cycle3)).to.be.greaterThan(Number(cycle2)); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.false; // It should be disabled at the start of the initial cycle (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledAtTimepoint(user.address, initialEmissionBlock)).to.be.false; // It should be enabled at the start of cycle 1 (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledAtTimepoint(user.address, cycle1EmissionBlock)).to.be.true; // But it should still be disabled at the start of cycle 2 (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledAtTimepoint(user.address, cycle2EmissionBlock)).to.be.false; // Auto-voting should be disabled at the start of cycle 3 (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledAtTimepoint(user.address, cycle3EmissionBlock)).to.be.false; }); (0, mocha_1.it)("should revert when user tries to toggle autovoting when has no votes available", async function () { 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); const user = otherAccounts[5]; await veBetterPassport.whitelist(user.address); await (0, chai_1.expect)(xAllocationVoting.connect(user).toggleAutoVoting(user.address)).to.be.revertedWith("AutoVotingLogic: at least 1 VOT3 is required"); }); (0, mocha_1.it)("should skip auto-vote and disable autovoting when user has below 1 VOT3 available", async function () { 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); // Register relayer await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); // User has 100 VOT3 initially (0, chai_1.expect)(await vot3.balanceOf(user.address)).to.equal(hardhat_1.ethers.parseEther("100")); // Set preferences and enable auto-voting await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Wait for next cycle for auto-voting to be active await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.true; // User transfers VOT3 to drop below 1 VOT3 (keeps only 0.5 VOT3) const recipient = otherAccounts[3]; await vot3.connect(user).transfer(recipient.address, hardhat_1.ethers.parseEther("99.5")); (0, chai_1.expect)(await vot3.balanceOf(user.address)).to.equal(hardhat_1.ethers.parseEther("0.5")); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId2 = await xAllocationVoting.currentRoundId(); // Relayer tries to cast vote // - should toggle off auto-voting // - should emit AutoVoteSkipped event // - should emit ExpectedActionsReduced event const castVoteTx = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId2); await (0, chai_1.expect)(castVoteTx) .to.emit(xAllocationVoting, "AutoVoteSkipped") .withArgs(user.address, roundId2, true, 0, hardhat_1.ethers.parseEther("0.5")); await (0, chai_1.expect)(castVoteTx).to.emit(relayerRewardsPool, "ExpectedActionsReduced").withArgs(roundId2, 2, 0, 0); // Reduced to 0 expected actions (0, chai_1.expect)(await relayerRewardsPool.getMissedAutoVotingUsersCount(roundId2)).to.equal(0); }); (0, mocha_1.it)("should revert non-relayers from claiming rewards during early access period for auto-voting users", async function () { const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(otherAccounts[0].address)); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, otherAccounts[0]); await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); const manualUser = user; const autoUser = user1; const nonRelayer = otherAccounts[3]; await veBetterPassport.whitelist(nonRelayer.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); await xAllocationVoting.connect(autoUser).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(autoUser).toggleAutoVoting(autoUser.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); const manualUserBalance = await vot3.balanceOf(manualUser.address); await xAllocationVoting.connect(manualUser).castVote(roundId, [app1Id], [manualUserBalance]); await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(autoUser.address, roundId); await (0, helpers_1.waitForRoundToEnd)(roundId); // Manual user can be claimed by anyone await (0, chai_1.expect)(voterRewards.connect(nonRelayer).claimReward(roundId, manualUser.address)).to.not.be.reverted; // Early access period is still active (0, chai_1.expect)(await relayerRewardsPool.isClaimEarlyAccessActive(roundId)).to.be.true; // Auto user cannot be claimed by non-relayer await (0, chai_1.expect)(voterRewards.connect(nonRelayer).claimReward(roundId, autoUser.address)).to.be.revertedWith("RelayerRewardsPool: caller is not a registered relayer during claim early access period"); // Auto user can be claimed by registered relayer during early access period await (0, chai_1.expect)(voterRewards.connect(relayer1).claimReward(roundId, autoUser.address)).to.not.be.reverted; }); (0, mocha_1.it)("should allow auto-voting users to claim rewards for themselves after early access period", async function () { const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(otherAccounts[0].address)); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, otherAccounts[0]); await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); const autoUser = user1; await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); await xAllocationVoting.connect(autoUser).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(autoUser).toggleAutoVoting(autoUser.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // Cast auto vote await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(autoUser.address, roundId); await (0, helpers_1.waitForRoundToEnd)(roundId); // Set early access period to 0 blocks to make it expire quickly await relayerRewardsPool.connect(owner).setEarlyAccessBlocks(0); // Verify early access period has ended (0, chai_1.expect)(await relayerRewardsPool.isClaimEarlyAccessActive(roundId)).to.be.false; // Auto user should now be able to claim their own rewards await (0, chai_1.expect)(voterRewards.connect(autoUser).claimReward(roundId, autoUser.address)).to.not.be.reverted; }); (0, mocha_1.it)("should skip auto-vote and disable autovoting when apps become ineligible, then allow user to re-enable with new apps", async function () { // This test verifies the path when: // 1. User has auto-voting enabled with app1 // 2. App1 becomes ineligible (auto-disabled) // 3. User adds new app2 to preferences // 4. User re-enables auto-voting // 5. User can successfully vote again with app2 await x2EarnCreatorContract.connect(owner).safeMint(appOwner.address); await x2EarnCreatorContract.connect(owner).safeMint(appOwner2.address); const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps.connect(appOwner).submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner); // Register relayer await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); // User sets preferences for app1 and enables auto-voting await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); // Wait for autovoting to be active await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); (0, chai_1.expect)(await xAllocationVoting.isEligibleForVote(app1Id, roundId)).to.be.true; // App gets unendorsed - will become ineligible after grace period const nodeId = await stargateNftMock.tokenOfOwnerByIndex(appOwner.address, 0); await x2EarnApps.connect(appOwner).unendorseApp(app1Id, nodeId, 0); (0, chai_1.expect)(await x2EarnApps.isAppUnendorsed(app1Id)).to.be.true; // Autovoting still works during current round await (0, chai_1.expect)(xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId)).to.not.be.reverted; // Fast forward through grace period (3 rounds) await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); await x2EarnApps.checkEndorsement(app1Id); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); await x2EarnApps.checkEndorsement(app1Id); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); await x2EarnApps.checkEndorsement(app1Id); // Now app1 is ineligible const roundId4 = await xAllocationVoting.currentRoundId(); (0, chai_1.expect)(await xAllocationVoting.isEligibleForVote(app1Id, roundId4)).to.be.false; // Relayer tries to vote - auto-voting gets disabled await (0, chai_1.expect)(xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId4)) .to.emit(xAllocationVoting, "AutoVoteSkipped") .withArgs(user.address, roundId4, true, 0, await vot3.balanceOf(user.address)); // Wait for next cycle await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Verify auto-voting is disabled and preferences cleared (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.false; (0, chai_1.expect)(await xAllocationVoting.getUserVotingPreferences(user.address)).to.deep.equal([]); // Recovery: User creates/adds a new app (app2) const app2Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner2.address)); await x2EarnApps .connect(appOwner2) .submitApp(appOwner2.address, appOwner2.address, appOwner2.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app2Id, appOwner2); // User sets preferences for new app2 await xAllocationVoting.connect(user).setUserVotingPreferences([app2Id]); (0, chai_1.expect)(await xAllocationVoting.getUserVotingPreferences(user.address)).to.deep.equal([app2Id]); // User re-enables auto-voting await (0, chai_1.expect)(xAllocationVoting.connect(user).toggleAutoVoting(user.address)) .to.emit(xAllocationVoting, "AutoVotingToggled") .withArgs(user.address, true); // Wait for next cycle for re-enabled status to be active await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId6 = await xAllocationVoting.currentRoundId(); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.true; (0, chai_1.expect)(await xAllocationVoting.isEligibleForVote(app2Id, roundId6)).to.be.true; // User can successfully vote again with new app const tx = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId6); await (0, chai_1.expect)(tx) .to.emit(xAllocationVoting, "AllocationAutoVoteCast") .withArgs(user.address, roundId6, [app2Id], [hardhat_1.ethers.parseEther("100")]); (0, chai_1.expect)(await xAllocationVoting.hasVoted(roundId6, user.address)).to.be.true; // Verify votes went to app2 (not app1) const app2Votes = await xAllocationVoting.getAppVotes(roundId6, app2Id); (0, chai_1.expect)(app2Votes).to.equal(hardhat_1.ethers.parseEther("100")); }); (0, mocha_1.it)("should skip auto-vote and disable autovoting when user drops below 1 VOT3 threshold", async function () { const recipient = otherAccounts[3]; 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); // Register relayer await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); (0, chai_1.expect)(await vot3.balanceOf(user.address)).to.equal(hardhat_1.ethers.parseEther("100")); // Set preferences first then enable autovoting await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.false; await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.true; // Transfer 99.5 VOT3 which would leave user with 0.5 VOT3 (below 1 VOT3) await vot3.connect(user).transfer(recipient.address, hardhat_1.ethers.parseEther("99.5")); (0, chai_1.expect)(await vot3.balanceOf(recipient.address)).to.equal(hardhat_1.ethers.parseEther("99.5")); // Verify autovoting is still enabled because the relayer has not cast the vote yet (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabled(user.address)).to.be.true; // Test multiple transfers with low balance - should NOT revert // Transfer 0.1 VOT3 more (user still has 0.4 VOT3, still below 1 VOT3) await vot3.connect(user).transfer(recipient.address, hardhat_1.ethers.parseEther("0.1")); (0, chai_1.expect)(await vot3.balanceOf(user.address)).to.equal(hardhat_1.ethers.parseEther("0.4")); (0, chai_1.expect)(await vot3.balanceOf(recipient.address)).to.equal(hardhat_1.ethers.parseEther("99.6")); // Another transfer - should still work without reverting await vot3.connect(user).transfer(recipient.address, hardhat_1.ethers.parseEther("0.1")); (0, chai_1.expect)(await vot3.balanceOf(user.address)).to.equal(hardhat_1.ethers.parseEther("0.3")); (0, chai_1.expect)(await vot3.balanceOf(recipient.address)).to.equal(hardhat_1.ethers.parseEther("99.7")); // Wait for next cycle for round snapshot to update await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Verify autovoting is still enabled because the relayer has not cast the vote yet (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabled(user.address)).to.be.true; (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabledInCurrentRound(user.address)).to.be.true; (0, chai_1.expect)(await vot3.balanceOf(user.address)).to.equal(hardhat_1.ethers.parseEther("0.3")); // Cast vote on behalf of user const roundId = await xAllocationVoting.currentRoundId(); const castVoteTx = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); await (0, chai_1.expect)(castVoteTx).to.emit(xAllocationVoting, "AutoVotingToggled").withArgs(user.address, false); await (0, chai_1.expect)(castVoteTx) .to.emit(xAllocationVoting, "AutoVoteSkipped") .withArgs(user.address, roundId, true, 0, await vot3.balanceOf(user.address)); // Verify autovoting is disabled (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabled(user.address)).to.be.false; }); (0, mocha_1.it)("should correctly report claim early access status throughout round lifecycle", async function () { const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(otherAccounts[0].address)); await x2EarnApps .connect(owner) .submitApp(otherAccounts[0].address, otherAccounts[0].address, otherAccounts[0].address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, otherAccounts[0]); await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); const autoUser = user1; await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); await xAllocationVoting.connect(autoUser).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(autoUser).toggleAutoVoting(autoUser.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); // During round - both vote and claim early access should be active (0, chai_1.expect)(await relayerRewardsPool.isVoteEarlyAccessActive(roundId)).to.be.true; (0, chai_1.expect)(await relayerRewardsPool.isClaimEarlyAccessActive(roundId)).to.be.true; // Cast auto vote and wait for round to end await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(autoUser.address, roundId); await (0, helpers_1.waitForRoundToEnd)(roundId); // After round ends (0, chai_1.expect)(await relayerRewardsPool.isClaimEarlyAccessActive(roundId)).to.be.true; // Set early access to 5 blocks and mine past it await relayerRewardsPool.connect(owner).setEarlyAccessBlocks(5); for (let i = 0; i < 6; i++) { await hardhat_1.ethers.provider.send("evm_mine", []); } // After claim early access expires - both should be false (0, chai_1.expect)(await relayerRewardsPool.isVoteEarlyAccessActive(roundId)).to.be.false; (0, chai_1.expect)(await relayerRewardsPool.isClaimEarlyAccessActive(roundId)).to.be.false; // Reset to default await relayerRewardsPool.connect(owner).setEarlyAccessBlocks(432000); }); }); (0, mocha_1.describe)("castVoteOnBehalfOf function", function () { (0, mocha_1.beforeEach)(async function () { await setupContracts(); // Give user voting power another 200 const startingAmount = "200"; await (0, helpers_1.getVot3Tokens)(user, startingAmount); // Now the user has 300 VOT3 await emissions.connect(minterAccount).start(); await relayerRewardsPool.connect(owner).registerRelayer(relayer1.address); }); (0, mocha_1.it)("should successfully cast vote on behalf of user with single app", async function () { 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 xAllocationVoting.connect(user).setUserVotingPreferences([app1Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); const tx = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); await (0, chai_1.expect)(tx).to.not.be.reverted; const hasVoted = await xAllocationVoting.hasVoted(roundId, user.address); (0, chai_1.expect)(hasVoted).to.be.true; }); (0, mocha_1.it)("should successfully cast vote on behalf of user with multiple apps and distribute votes equally", async function () { await x2EarnCreatorContract.connect(owner).safeMint(appOwner1.address); await x2EarnCreatorContract.connect(owner).safeMint(appOwner2.address); await x2EarnCreatorContract.connect(owner).safeMint(appOwner3.address); const app1Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner1.address)); const app2Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner2.address)); const app3Id = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner3.address)); await x2EarnApps .connect(appOwner1) .submitApp(appOwner1.address, appOwner1.address, appOwner1.address, "metadataURI"); await x2EarnApps .connect(appOwner2) .submitApp(appOwner2.address, appOwner2.address, appOwner2.address, "metadataURI"); await x2EarnApps .connect(appOwner3) .submitApp(appOwner3.address, appOwner3.address, appOwner3.address, "metadataURI"); await (0, xnodes_1.endorseApp)(app1Id, appOwner1); await (0, xnodes_1.endorseApp)(app2Id, appOwner2); await (0, xnodes_1.endorseApp)(app3Id, appOwner3); // Set preferences first then enable autovoting for all apps await xAllocationVoting.connect(user).setUserVotingPreferences([app1Id, app2Id, app3Id]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); const tx = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); await (0, chai_1.expect)(tx).to.not.be.reverted; // Verify vote was cast const hasVoted = await xAllocationVoting.hasVoted(roundId, user.address); (0, chai_1.expect)(hasVoted).to.be.true; // Verify votes were distributed equally (100 VOT3 each) const expectedVotePerApp = hardhat_1.ethers.parseEther("100"); const app1Votes = await xAllocationVoting.getAppVotes(roundId, app1Id); const app2Votes = await xAllocationVoting.getAppVotes(roundId, app2Id); const app3Votes = await xAllocationVoting.getAppVotes(roundId, app3Id); (0, chai_1.expect)(app1Votes).to.equal(expectedVotePerApp); (0, chai_1.expect)(app2Votes).to.equal(expectedVotePerApp); (0, chai_1.expect)(app3Votes).to.equal(expectedVotePerApp); }); (0, mocha_1.it)("should successfully cast vote on behalf of user with 15 apps and distribute votes equally", async function () { const appIds = []; const getAllSigners = await hardhat_1.ethers.getSigners(); const getAllAppOwners = getAllSigners.slice(5, 20); for (let i = 0; i < 15; i++) { const appOwner = getAllAppOwners[i]; const isCreatorMinted = await x2EarnCreatorContract.balanceOf(appOwner.address); if (isCreatorMinted === 0n) { await x2EarnCreatorContract.connect(owner).safeMint(appOwner.address); } const appId = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps .connect(appOwner) .submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); // Endorse the app await (0, xnodes_1.endorseApp)(appId, appOwner); appIds.push(appId); } // Set preferences first then enable autovoting for all apps await xAllocationVoting.connect(user).setUserVotingPreferences(appIds); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); const roundId = await xAllocationVoting.currentRoundId(); const tx = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); await (0, chai_1.expect)(tx).to.not.be.reverted; // Verify vote was cast const hasVoted = await xAllocationVoting.hasVoted(roundId, user.address); (0, chai_1.expect)(hasVoted).to.be.true; // Verify votes were distributed equally (20 VOT3 each = 300 VOT3 total / 15 apps) const expectedVotePerApp = hardhat_1.ethers.parseEther("20"); for (const appId of appIds) { const votes = await xAllocationVoting.getAppVotes(roundId, appId); (0, chai_1.expect)(votes).to.equal(expectedVotePerApp); } }); mocha_1.it.skip("should show gas cost progression from 1 to 15 apps", async function () { /** * This test is for gas cost analysis. */ const appIds = []; const getAllSigners = await hardhat_1.ethers.getSigners(); const getAllAppOwners = getAllSigners.slice(5, 20); for (let i = 0; i < 15; i++) { const appOwner = getAllAppOwners[i]; const isCreatorMinted = await x2EarnCreatorContract.balanceOf(appOwner.address); if (isCreatorMinted === 0n) { await x2EarnCreatorContract.connect(owner).safeMint(appOwner.address); } const appId = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps .connect(appOwner) .submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); // Endorse the app await (0, xnodes_1.endorseApp)(appId, appOwner); appIds.push(appId); } const gasResults = []; // Enable autovoting once at the beginning (need to set preferences first) await xAllocationVoting.connect(user).setUserVotingPreferences([appIds[0]]); await xAllocationVoting.connect(user).toggleAutoVoting(user.address); await (0, helpers_1.waitForNextCycle)(emissions); await emissions.connect(minterAccount).distribute(); // Test from 1 to 15 apps for (let appCount = 1; appCount <= 15; appCount++) { // Update voting preferences with apps 0 to appCount-1 const currentAppIds = appIds.slice(0, appCount); await xAllocationVoting.connect(user).setUserVotingPreferences(currentAppIds); // Start new round const roundId = await xAllocationVoting.currentRoundId(); // Cast vote and measure gas const tx = await xAllocationVoting.connect(relayer1).castVoteOnBehalfOf(user.address, roundId); const receipt = await tx.wait(); await (0, chai_1.expect)(tx).to.not.be.reverted; const gasUsed = receipt?.gasUsed || 0n; gasResults.push({ appCount, gasUsed }); console.log(`${appCount} apps: ${gasUsed.toLocaleString()} gas`); // Verify vote was cast const hasVoted = await xAllocationVoting.hasVoted(roundId, user.address); (0, chai_1.expect)(hasVoted).to.be.true; // Wait for round to end and start next round (except for last iteration) if (appCount < 15) { await (0, helpers_1.waitForRoundToEnd)(roundId); await (0, helpers_1.startNewAllocationRound)(); } } // Calculate gas increase rate console.log("\n=== Gas Increase Analysis ==="); for (let i = 1; i < gasResults.length; i++) { const prev = gasResults[i - 1]; const curr = gasResults[i]; const increase = Number(curr.gasUsed - prev.gasUsed); console.log(`${prev.appCount} → ${curr.appCount} apps: +${increase.toLocaleString()} gas`); } }); (0, mocha_1.it)("should revert when user tries to set more than 15 app preferences", async function () { const appIds = []; const getAllSigners = await hardhat_1.ethers.getSigners(); const getAllAppOwners = getAllSigners.slice(4, 20); // 16 apps for (let i = 0; i < getAllAppOwners.length; i++) { const appOwner = getAllAppOwners[i]; const isCreatorMinted = await x2EarnCreatorContract.balanceOf(appOwner.address); if (isCreatorMinted === 0n) { await x2EarnCreatorContract.connect(owner).safeMint(appOwner.address); } const appId = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(appOwner.address)); await x2EarnApps .connect(appOwner) .submitApp(appOwner.address, appOwner.address, appOwner.address, "metadataURI"); // Endorse the app await (0, xnodes_1.endorseApp)(appId, appOwner); appIds.push(appId); } await (0, chai_1.expect)(xAllocationVoting.connect(user).setUserVotingPreferences(appIds)).to.be.revertedWith("AutoVotingLogic: must vote for less than 15 apps"); }); (0, mocha_1.it)("should revert when voter is not a person", async function () { // User is non-person without whitelisted const user = otherAccounts[5]; 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, chai_1.expect)(xAllocationVoting.connect(user).toggleAutoVoting(user.address)).to.be.revertedWithCustomError(xAllocationVoting, "GovernorPersonhoodVerificationFailed"); }); (0, mocha_1.it)("should revert when user has no app preferences set", async function () { 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); // Try to enable autovoting but don't set preferences (should fail) await (0, chai_1.expect)(xAllocationVoting.connect(user).toggleAutoVoting(user.address)).to.be.revertedWith("AutoVotingLogic: must select at least one app"); }); (0, mocha_1.it)("[Edge Case] should handle vote distribution with remaining dust correctly", async function () { // User1 has 100 VOT3 that doesn't divide evenly by 3 (100 / 3 = 33.33...) await x2EarnCreatorContract.connect(owner).safeMint(appOwner1.address); await x2EarnCreatorContract.connect(owner).safeMint(appOwner2.address); await x2EarnCreatorContract.connect(owner).safeMint(appOwner3.address);