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