@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
451 lines (450 loc) • 30.3 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const hardhat_1 = require("hardhat");
const chai_1 = require("chai");
const mocha_1 = require("mocha");
const deploy_1 = require("../helpers/deploy");
const common_1 = require("../helpers/common");
const xnodes_1 = require("../helpers/xnodes");
(0, mocha_1.describe)("NavigatorRegistry Security - @shard19h", function () {
let navigatorRegistry;
let b3tr;
let vot3;
let xAllocationVoting;
let emissions;
let voterRewards;
let x2EarnApps;
let x2EarnCreator;
let veBetterPassport;
let relayerRewardsPool;
let owner;
let minterAccount;
let otherAccounts;
let creators;
const STAKE_AMOUNT = hardhat_1.ethers.parseEther("50000");
const METADATA_URI = "ipfs://navigator-metadata";
// Helper: fund account with B3TR and approve NavigatorRegistry
const fundAndApprove = async (account, amount) => {
await b3tr.connect(owner).transfer(account.address, amount);
const registryAddress = await navigatorRegistry.getAddress();
await b3tr.connect(account).approve(registryAddress, amount);
};
// Helper: register a navigator with default stake
const registerNavigator = async (account, amount = STAKE_AMOUNT) => {
await fundAndApprove(account, amount);
await navigatorRegistry.connect(account).register(amount, METADATA_URI);
};
// Helper: delegate VOT3 to a navigator
const delegateCitizen = async (citizen, navigator, amount) => {
await (0, common_1.getVot3Tokens)(citizen, amount);
await navigatorRegistry.connect(citizen).delegate(navigator.address, hardhat_1.ethers.parseEther(amount));
};
// Helper: impersonate VoterRewards and deposit a fee
const depositFeeViaImpersonation = async (navigator, roundId, amount) => {
const voterRewardsAddress = await voterRewards.getAddress();
await hardhat_1.ethers.provider.send("hardhat_impersonateAccount", [voterRewardsAddress]);
await hardhat_1.ethers.provider.send("hardhat_setBalance", [voterRewardsAddress, "0x" + (10n ** 18n).toString(16)]);
const voterRewardsSigner = await hardhat_1.ethers.getSigner(voterRewardsAddress);
const registryAddress = await navigatorRegistry.getAddress();
await b3tr.connect(owner).transfer(registryAddress, amount);
await navigatorRegistry.connect(voterRewardsSigner).depositNavigatorFee(navigator, roundId, amount);
await hardhat_1.ethers.provider.send("hardhat_stopImpersonatingAccount", [voterRewardsAddress]);
};
// Helper: advance N allocation rounds
const advanceRounds = async (count) => {
for (let i = 0; i < count; i++) {
const roundId = await xAllocationVoting.currentRoundId();
await (0, common_1.waitForRoundToEnd)(Number(roundId));
await emissions.distribute();
}
};
// Helper: create a real endorsed app
const createEndorsedApp = async (creator, endorser) => {
if ((await x2EarnCreator.balanceOf(creator.address)) === 0n) {
await x2EarnCreator.connect(owner).safeMint(creator.address);
}
const appName = "SecurityTestApp" + Math.random();
await x2EarnApps.connect(creator).submitApp(creator.address, creator.address, appName, "metadataURI");
const appId = await x2EarnApps.hashAppName(appName);
await (0, xnodes_1.endorseApp)(appId, endorser);
return appId;
};
(0, mocha_1.beforeEach)(async function () {
const deployment = await (0, deploy_1.getOrDeployContractInstances)({ forceDeploy: true });
if (!deployment)
throw new Error("Failed to deploy contracts");
navigatorRegistry = deployment.navigatorRegistry;
b3tr = deployment.b3tr;
vot3 = deployment.vot3;
xAllocationVoting = deployment.xAllocationVoting;
emissions = deployment.emissions;
voterRewards = deployment.voterRewards;
x2EarnApps = deployment.x2EarnApps;
x2EarnCreator = deployment.x2EarnCreator;
veBetterPassport = deployment.veBetterPassport;
relayerRewardsPool = deployment.relayerRewardsPool;
owner = deployment.owner;
minterAccount = deployment.minterAccount;
otherAccounts = deployment.otherAccounts;
creators = deployment.creators;
// Mint B3TR to owner for transfers
await b3tr.connect(minterAccount).mint(owner.address, hardhat_1.ethers.parseEther("10000000"));
// Ensure VOT3 supply exists (max stake = 1% of VOT3 supply, need >= 5M for 50k stake)
await (0, common_1.getVot3Tokens)(otherAccounts[15], "10000000");
// Disable early access period so castNavigatorVote doesn't require a registered relayer
await relayerRewardsPool.connect(owner).setEarlyAccessBlocks(0);
});
// ======================== 1. Malicious User ======================== //
(0, mocha_1.describe)("Malicious user", function () {
(0, mocha_1.it)("delegate small amount VOT3: castNavigatorVote uses only delegated voting power", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
// Citizen gets 1000 VOT3 but delegates only 2 (above voting threshold, but far below total balance)
const smallAmount = hardhat_1.ethers.parseEther("2");
await (0, common_1.getVot3Tokens)(citizen, "1000");
await navigatorRegistry.connect(citizen).delegate(navigator1.address, smallAmount);
// Create a real endorsed app and start a round
const appId = await createEndorsedApp(creators[0], otherAccounts[8]);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Navigator sets preferences
await navigatorRegistry.connect(navigator1).setAllocationPreferences(roundId, [appId], [10000]);
// Whitelist citizen for passport check
await veBetterPassport.connect(owner).whitelist(citizen.address);
if (!(await veBetterPassport.isCheckEnabled(1)))
await veBetterPassport.toggleCheck(1);
// Cast navigator vote for citizen
await xAllocationVoting.castNavigatorVote(citizen.address, roundId);
// Verify votes received equals only the delegated amount, not the full 1000 VOT3 balance
const snapshot = await xAllocationVoting.roundSnapshot(roundId);
const delegatedPower = await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen.address, snapshot);
const votesReceived = await xAllocationVoting.getAppVotes(roundId, appId);
(0, chai_1.expect)(votesReceived).to.equal(delegatedPower);
(0, chai_1.expect)(votesReceived).to.be.lt(hardhat_1.ethers.parseEther("1000"));
});
(0, mocha_1.it)("transfer locked VOT3: delegate 500, try transfer 501, reverts", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
// Citizen gets 1000 VOT3, delegates 500
await (0, common_1.getVot3Tokens)(citizen, "1000");
const delegateAmount = hardhat_1.ethers.parseEther("500");
await navigatorRegistry.connect(citizen).delegate(navigator1.address, delegateAmount);
// Try to transfer 501 VOT3 (exceeds unlocked balance)
const transferAmount = hardhat_1.ethers.parseEther("501");
await (0, chai_1.expect)(vot3.connect(citizen).transfer(otherAccounts[12].address, transferAmount)).to.be.revertedWith("VOT3: transfer exceeds unlocked balance");
});
(0, mocha_1.it)("call depositNavigatorFee directly (not VoterRewards): reverts UnauthorizedCaller", async function () {
const navigator1 = otherAccounts[10];
await registerNavigator(navigator1);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
await (0, chai_1.expect)(navigatorRegistry.connect(otherAccounts[11]).depositNavigatorFee(navigator1.address, roundId, 100n)).to.be.revertedWithCustomError(navigatorRegistry, "UnauthorizedCaller");
});
(0, mocha_1.it)("non-navigator tries to claim fees: reverts NotRegistered", async function () {
const nonNavigator = otherAccounts[11];
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
await (0, chai_1.expect)(navigatorRegistry.connect(nonNavigator).claimFee(roundId)).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered");
});
(0, mocha_1.it)("regular user tries to call onlyNavigator functions: reverts", async function () {
const user = otherAccounts[11];
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
const app1 = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("App1"));
// setAllocationPreferences
await (0, chai_1.expect)(navigatorRegistry.connect(user).setAllocationPreferences(roundId, [app1], [10000])).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered");
// setProposalDecision
await (0, chai_1.expect)(navigatorRegistry.connect(user).setProposalDecision(1, 2)).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered");
// announceExit
await (0, chai_1.expect)(navigatorRegistry.connect(user).announceExit()).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered");
});
});
// ======================== 2. Malicious Navigator ======================== //
(0, mocha_1.describe)("Malicious navigator", function () {
(0, mocha_1.it)("claim fees from round with no deposits: reverts NoFeesToClaim", async function () {
const navigator1 = otherAccounts[10];
await registerNavigator(navigator1);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Advance past lock period
const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod());
await advanceRounds(lockPeriod);
await (0, chai_1.expect)(navigatorRegistry.connect(navigator1).claimFee(roundId)).to.be.revertedWithCustomError(navigatorRegistry, "NoFeesToClaim");
});
(0, mocha_1.it)("claim before lock period: reverts FeesStillLocked", async function () {
const navigator1 = otherAccounts[10];
await registerNavigator(navigator1);
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
await depositFeeViaImpersonation(navigator1.address, depositRoundId, hardhat_1.ethers.parseEther("100"));
// Advance only 2 rounds (less than feeLockPeriod of 4)
await advanceRounds(2);
await (0, chai_1.expect)(navigatorRegistry.connect(navigator1).claimFee(depositRoundId)).to.be.revertedWithCustomError(navigatorRegistry, "FeesStillLocked");
});
(0, mocha_1.it)("report same round twice: second reportRoundInfractions reverts AlreadySlashed", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
// Citizen delegates to navigator (so navigator has citizens)
await (0, common_1.getVot3Tokens)(citizen, "500");
await navigatorRegistry.connect(citizen).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"));
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Wait for round to end without navigator setting preferences
await (0, common_1.waitForRoundToEnd)(Number(roundId));
// First report succeeds
await navigatorRegistry.reportRoundInfractions(navigator1.address, roundId, []);
// Second report reverts
await (0, chai_1.expect)(navigatorRegistry.reportRoundInfractions(navigator1.address, roundId, [])).to.be.revertedWithCustomError(navigatorRegistry, "AlreadySlashed");
});
(0, mocha_1.it)("set preferences with 100% to one app [10000]: valid, vote goes entirely to that app", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
// Citizen delegates
const delegateAmount = "500";
await delegateCitizen(citizen, navigator1, delegateAmount);
// Create real app and start round
const appId = await createEndorsedApp(creators[0], otherAccounts[8]);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Navigator sets 100% to one app
await navigatorRegistry.connect(navigator1).setAllocationPreferences(roundId, [appId], [10000]);
// Whitelist citizen
await veBetterPassport.connect(owner).whitelist(citizen.address);
if (!(await veBetterPassport.isCheckEnabled(1)))
await veBetterPassport.toggleCheck(1);
// Cast navigator vote
await xAllocationVoting.castNavigatorVote(citizen.address, roundId);
// Verify all voting power went to the single app
const snapshot = await xAllocationVoting.roundSnapshot(roundId);
const delegatedPower = await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen.address, snapshot);
const appVotes = await xAllocationVoting.getAppVotes(roundId, appId);
(0, chai_1.expect)(appVotes).to.equal(delegatedPower);
});
});
// ======================== 3. Malicious Relayer ======================== //
(0, mocha_1.describe)("Malicious relayer", function () {
(0, mocha_1.it)("castNavigatorVote for random address (not delegated): reverts NotDelegatedToNavigator", async function () {
const randomUser = otherAccounts[11];
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
await (0, chai_1.expect)(xAllocationVoting.castNavigatorVote(randomUser.address, roundId)).to.be.revertedWithCustomError(xAllocationVoting, "NotDelegatedToNavigator");
});
(0, mocha_1.it)("castNavigatorVote before navigator sets preferences: skips vote (after skip window)", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
await delegateCitizen(citizen, navigator1, "500");
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Advance past the skip window so skip is permitted
const skipWindow = await xAllocationVoting.citizenSkipWindowBlocks();
const deadline = await xAllocationVoting.roundDeadline(roundId);
const currentBlock = BigInt(await hardhat_1.ethers.provider.getBlockNumber());
const blocksToMine = deadline - currentBlock - skipWindow;
if (blocksToMine > 0n)
await (0, common_1.moveBlocks)(Number(blocksToMine));
await (0, chai_1.expect)(xAllocationVoting.castNavigatorVote(citizen.address, roundId)).to.emit(xAllocationVoting, "NavigatorVoteSkipped");
});
(0, mocha_1.it)("call depositNavigatorFee from non-VoterRewards address: reverts UnauthorizedCaller", async function () {
const navigator1 = otherAccounts[10];
await registerNavigator(navigator1);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Any external caller (not VoterRewards) should be rejected
await (0, chai_1.expect)(navigatorRegistry.connect(owner).depositNavigatorFee(navigator1.address, roundId, hardhat_1.ethers.parseEther("10"))).to.be.revertedWithCustomError(navigatorRegistry, "UnauthorizedCaller");
});
});
// ======================== 4. Access Control ======================== //
(0, mocha_1.describe)("Access control", function () {
(0, mocha_1.it)("non-governance calling setMinStake: reverts", async function () {
const attacker = otherAccounts[11];
await (0, chai_1.expect)(navigatorRegistry.connect(attacker).setMinStake(1000n)).to.be.reverted;
});
(0, mocha_1.it)("non-admin calling setXAllocationVoting: reverts", async function () {
const attacker = otherAccounts[11];
await (0, chai_1.expect)(navigatorRegistry.connect(attacker).setXAllocationVoting(attacker.address)).to.be.reverted;
});
(0, mocha_1.it)("non-navigator calling setAllocationPreferences: reverts", async function () {
const attacker = otherAccounts[11];
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
const app1 = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("App1"));
await (0, chai_1.expect)(navigatorRegistry.connect(attacker).setAllocationPreferences(roundId, [app1], [10000])).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered");
});
(0, mocha_1.it)("non-navigator calling claimFee: reverts", async function () {
const attacker = otherAccounts[11];
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
await (0, chai_1.expect)(navigatorRegistry.connect(attacker).claimFee(roundId)).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered");
});
(0, mocha_1.it)("non-navigator calling announceExit: reverts", async function () {
const attacker = otherAccounts[11];
await (0, common_1.bootstrapAndStartEmissions)();
await (0, chai_1.expect)(navigatorRegistry.connect(attacker).announceExit()).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered");
});
(0, mocha_1.it)("non-voterRewards calling depositNavigatorFee: reverts UnauthorizedCaller", async function () {
const navigator1 = otherAccounts[10];
await registerNavigator(navigator1);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
await (0, chai_1.expect)(navigatorRegistry.connect(otherAccounts[12]).depositNavigatorFee(navigator1.address, roundId, 100n)).to.be.revertedWithCustomError(navigatorRegistry, "UnauthorizedCaller");
});
});
// ======================== 5. Numerical Correctness ======================== //
(0, mocha_1.describe)("Numerical correctness", function () {
(0, mocha_1.it)("large delegation 400K VOT3: percentages [6000, 4000] convert correctly to weights", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
// Citizen gets and delegates 400K VOT3 (within 500K capacity of 50K stake)
const delegateAmount = "400000";
await delegateCitizen(citizen, navigator1, delegateAmount);
// Create two real endorsed apps
const appId1 = await createEndorsedApp(creators[0], otherAccounts[8]);
const appId2 = await createEndorsedApp(creators[1], otherAccounts[9]);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Navigator sets 60/40 split
await navigatorRegistry.connect(navigator1).setAllocationPreferences(roundId, [appId1, appId2], [6000, 4000]);
// Whitelist citizen
await veBetterPassport.connect(owner).whitelist(citizen.address);
if (!(await veBetterPassport.isCheckEnabled(1)))
await veBetterPassport.toggleCheck(1);
// Cast navigator vote
await xAllocationVoting.castNavigatorVote(citizen.address, roundId);
// Verify: 400K * 60% = 240k, 400K * 40% = 160k
const snapshot = await xAllocationVoting.roundSnapshot(roundId);
const delegatedPower = await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen.address, snapshot);
const expectedApp1 = (delegatedPower * 6000n) / 10000n;
const expectedApp2 = (delegatedPower * 4000n) / 10000n;
const app1Votes = await xAllocationVoting.getAppVotes(roundId, appId1);
const app2Votes = await xAllocationVoting.getAppVotes(roundId, appId2);
// Dust assigned to first app, so app1 >= expected, total = delegatedPower
(0, chai_1.expect)(app1Votes).to.be.gte(expectedApp1);
(0, chai_1.expect)(app2Votes).to.equal(expectedApp2);
(0, chai_1.expect)(app1Votes + app2Votes).to.equal(delegatedPower);
});
(0, mocha_1.it)("multiple citizens same navigator: each gets independent vote with own delegated power", async function () {
const navigator1 = otherAccounts[10];
const citizen1 = otherAccounts[11];
const citizen2 = otherAccounts[12];
await registerNavigator(navigator1);
// Two citizens delegate different amounts
await delegateCitizen(citizen1, navigator1, "1000");
await delegateCitizen(citizen2, navigator1, "3000");
// Create an endorsed app
const appId = await createEndorsedApp(creators[0], otherAccounts[8]);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Navigator sets preferences
await navigatorRegistry.connect(navigator1).setAllocationPreferences(roundId, [appId], [10000]);
// Whitelist both citizens
await veBetterPassport.connect(owner).whitelist(citizen1.address);
await veBetterPassport.connect(owner).whitelist(citizen2.address);
if (!(await veBetterPassport.isCheckEnabled(1)))
await veBetterPassport.toggleCheck(1);
// Cast votes for both citizens
await xAllocationVoting.castNavigatorVote(citizen1.address, roundId);
await xAllocationVoting.castNavigatorVote(citizen2.address, roundId);
// Verify total votes = citizen1 delegated + citizen2 delegated
const snapshot = await xAllocationVoting.roundSnapshot(roundId);
const power1 = await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen1.address, snapshot);
const power2 = await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen2.address, snapshot);
const totalVotes = await xAllocationVoting.getAppVotes(roundId, appId);
(0, chai_1.expect)(totalVotes).to.equal(power1 + power2);
});
(0, mocha_1.it)("fee math: citizen reward 1000, navigator fee 20% = 200, relayer fee 10% of 800 = 80, citizen gets 720", async function () {
// This test verifies the fee ordering: navigator fee first, then relayer fee on remainder
const grossReward = 1000n;
const navigatorFeePercent = 2000n; // 20% (in basis points)
const relayerFeePercent = 1000n; // 10% (in basis points)
const BASIS_POINTS = 10000n;
// Navigator fee = grossReward * 20% = 200
const navigatorFee = (grossReward * navigatorFeePercent) / BASIS_POINTS;
(0, chai_1.expect)(navigatorFee).to.equal(200n);
// After navigator fee
const afterNavFee = grossReward - navigatorFee;
(0, chai_1.expect)(afterNavFee).to.equal(800n);
// Relayer fee = afterNavFee * 10% = 80
const relayerFee = (afterNavFee * relayerFeePercent) / BASIS_POINTS;
(0, chai_1.expect)(relayerFee).to.equal(80n);
// Citizen net = 800 - 80 = 720
const citizenNet = afterNavFee - relayerFee;
(0, chai_1.expect)(citizenNet).to.equal(720n);
// Sanity: all parts sum to original
(0, chai_1.expect)(navigatorFee + relayerFee + citizenNet).to.equal(grossReward);
});
});
// ======================== 6. Contract-Level Protections ======================== //
(0, mocha_1.describe)("Contract-level protections", function () {
(0, mocha_1.it)("self-delegation: navigator cannot delegate to themselves", async function () {
const navigator1 = otherAccounts[10];
await registerNavigator(navigator1);
await (0, common_1.getVot3Tokens)(navigator1, "1000");
await (0, chai_1.expect)(navigatorRegistry.connect(navigator1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"))).to.be.revertedWithCustomError(navigatorRegistry, "SelfDelegationNotAllowed");
});
(0, mocha_1.it)("delegated citizen cannot castVote manually on XAllocationVoting", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
await (0, common_1.getVot3Tokens)(citizen, "1000");
await navigatorRegistry.connect(citizen).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"));
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Whitelist citizen for personhood
await veBetterPassport.connect(owner).whitelist(citizen.address);
const appId = hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes("TestApp"));
await (0, chai_1.expect)(xAllocationVoting.connect(citizen).castVote(roundId, [appId], [hardhat_1.ethers.parseEther("100")])).to.be.revertedWithCustomError(xAllocationVoting, "DelegatedToNavigator");
});
(0, mocha_1.it)("navigator cannot enable auto-voting", async function () {
const navigator1 = otherAccounts[10];
await registerNavigator(navigator1);
await (0, chai_1.expect)(xAllocationVoting.connect(navigator1).toggleAutoVoting(navigator1.address)).to.be.revertedWithCustomError(xAllocationVoting, "NavigatorCannotEnableAutoVoting");
});
(0, mocha_1.it)("delegated citizen cannot enable auto-voting", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
await (0, common_1.getVot3Tokens)(citizen, "1000");
await navigatorRegistry.connect(citizen).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"));
await (0, chai_1.expect)(xAllocationVoting.connect(citizen).toggleAutoVoting(citizen.address)).to.be.revertedWithCustomError(xAllocationVoting, "DelegatedToNavigator");
});
(0, mocha_1.it)("double voting blocked: navigator votes for citizen, citizen cannot vote again", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
await (0, common_1.getVot3Tokens)(citizen, "1000");
await navigatorRegistry.connect(citizen).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"));
// Create endorsed app BEFORE starting emissions (so it's eligible in round 1)
const appId = await createEndorsedApp(creators[0], otherAccounts[8]);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Navigator sets preferences and casts vote for citizen
await navigatorRegistry.connect(navigator1).setAllocationPreferences(roundId, [appId], [10000]);
await veBetterPassport.connect(owner).whitelist(citizen.address);
await xAllocationVoting.castNavigatorVote(citizen.address, roundId);
// Citizen tries to vote manually — blocked by DelegatedToNavigator (not just hasVoted)
await (0, chai_1.expect)(xAllocationVoting.connect(citizen).castVote(roundId, [appId], [hardhat_1.ethers.parseEther("100")])).to.be.revertedWithCustomError(xAllocationVoting, "DelegatedToNavigator");
});
(0, mocha_1.it)("re-delegate after undelegate: checkpoints correct", async function () {
const navigator1 = otherAccounts[10];
const citizen = otherAccounts[11];
await registerNavigator(navigator1);
await (0, common_1.getVot3Tokens)(citizen, "1000");
// Delegate
await navigatorRegistry.connect(citizen).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"));
(0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen.address)).to.equal(hardhat_1.ethers.parseEther("500"));
// Undelegate
await navigatorRegistry.connect(citizen).undelegate();
(0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen.address)).to.equal(0);
// Re-delegate different amount
await navigatorRegistry.connect(citizen).delegate(navigator1.address, hardhat_1.ethers.parseEther("300"));
(0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen.address)).to.equal(hardhat_1.ethers.parseEther("300"));
(0, chai_1.expect)(await navigatorRegistry.getNavigator(citizen.address)).to.equal(navigator1.address);
});
});
});