UNPKG

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