UNPKG

@vechain/vebetterdao-contracts

Version:

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

267 lines (266 loc) 18.9 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"); (0, mocha_1.describe)("NavigatorRegistry Lifecycle - @shard19f", function () { let navigatorRegistry; let b3tr; let xAllocationVoting; let emissions; let owner; let minterAccount; let otherAccounts; let navigator1; let nonNavigator; 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) => { await fundAndApprove(account, STAKE_AMOUNT); await navigatorRegistry.connect(account).register(STAKE_AMOUNT, METADATA_URI); }; (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; xAllocationVoting = deployment.xAllocationVoting; emissions = deployment.emissions; owner = deployment.owner; minterAccount = deployment.minterAccount; otherAccounts = deployment.otherAccounts; navigator1 = otherAccounts[10]; nonNavigator = otherAccounts[11]; // Mint B3TR to owner for navigator registration transfers await b3tr.connect(minterAccount).mint(owner.address, hardhat_1.ethers.parseEther("10000000")); // Create VOT3 supply (max stake = 1% of VOT3 supply, so need >= 5M VOT3 for 50k stake) await (0, common_1.getVot3Tokens)(otherAccounts[15], "10000000"); // Register navigator await registerNavigator(navigator1); // Start emissions so rounds exist await (0, common_1.bootstrapAndStartEmissions)(); }); // ======================== 1. announceExit() ======================== // (0, mocha_1.describe)("announceExit()", function () { (0, mocha_1.it)("happy path: sets isExiting and emits ExitAnnounced with correct rounds", async function () { const currentRound = await xAllocationVoting.currentRoundId(); const currentDeadline = await xAllocationVoting.roundDeadline(currentRound); const votingPeriod = await xAllocationVoting.votingPeriod(); const exitNoticePeriod = await navigatorRegistry.getExitNoticePeriod(); const expectedDeadline = currentDeadline + votingPeriod * exitNoticePeriod; const tx = await navigatorRegistry.connect(navigator1).announceExit(); await (0, chai_1.expect)(tx) .to.emit(navigatorRegistry, "ExitAnnounced") .withArgs(navigator1.address, currentRound, expectedDeadline); (0, chai_1.expect)(await navigatorRegistry.isExiting(navigator1.address)).to.be.true; }); (0, mocha_1.it)("reverts if not registered (onlyNavigator)", async function () { await (0, chai_1.expect)(navigatorRegistry.connect(nonNavigator).announceExit()).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered"); }); (0, mocha_1.it)("reverts if already exiting", async function () { await navigatorRegistry.connect(navigator1).announceExit(); await (0, chai_1.expect)(navigatorRegistry.connect(navigator1).announceExit()).to.be.revertedWithCustomError(navigatorRegistry, "AlreadyExiting"); }); (0, mocha_1.it)("reverts if navigator is deactivated (onlyNavigator)", async function () { await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await (0, chai_1.expect)(navigatorRegistry.connect(navigator1).announceExit()).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered"); }); }); // ======================== 1b. isNavigator after exit ======================== // (0, mocha_1.describe)("isNavigator() after voluntary exit", function () { 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.connect(minterAccount).distribute(); } }; (0, mocha_1.it)("returns true while EXITING (before deadline)", async function () { await navigatorRegistry.connect(navigator1).announceExit(); (0, chai_1.expect)(await navigatorRegistry.isExiting(navigator1.address)).to.be.true; (0, chai_1.expect)(await navigatorRegistry.isNavigator(navigator1.address)).to.be.true; }); (0, mocha_1.it)("returns false after exit deadline passes", async function () { await navigatorRegistry.connect(navigator1).announceExit(); const exitNoticePeriod = await navigatorRegistry.getExitNoticePeriod(); // Advance past the notice period so the checkpoint deadline is reached await advanceRounds(Number(exitNoticePeriod) + 1); (0, chai_1.expect)(await navigatorRegistry.getStatus(navigator1.address)).to.equal(3); // DEACTIVATED (0, chai_1.expect)(await navigatorRegistry.isNavigator(navigator1.address)).to.be.false; }); (0, mocha_1.it)("returns false after governance deactivation", async function () { await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); (0, chai_1.expect)(await navigatorRegistry.isNavigator(navigator1.address)).to.be.false; }); }); // ======================== 2. Deactivation ======================== // (0, mocha_1.describe)("Deactivation", function () { (0, mocha_1.it)("deactivateNavigator: sets isDeactivated and emits NavigatorDeactivatedEvent", async function () { const tx = await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await (0, chai_1.expect)(tx).to.emit(navigatorRegistry, "NavigatorDeactivatedEvent").withArgs(navigator1.address, 0); (0, chai_1.expect)(await navigatorRegistry.isDeactivated(navigator1.address)).to.be.true; }); (0, mocha_1.it)("can withdrawStake after deactivation", async function () { await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); const stakeBefore = await navigatorRegistry.getStake(navigator1.address); await navigatorRegistry.connect(navigator1).withdrawStake(stakeBefore); (0, chai_1.expect)(await navigatorRegistry.getStake(navigator1.address)).to.equal(0n); }); (0, mocha_1.it)("cannot register again with same account (AlreadyRegistered)", async function () { await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await fundAndApprove(navigator1, STAKE_AMOUNT); await (0, chai_1.expect)(navigatorRegistry.connect(navigator1).register(STAKE_AMOUNT, METADATA_URI)).to.be.revertedWithCustomError(navigatorRegistry, "AlreadyRegistered"); }); }); // ======================== 5. setMetadataURI() ======================== // (0, mocha_1.describe)("setMetadataURI()", function () { (0, mocha_1.it)("happy path: updates URI and emits MetadataURIUpdated", async function () { const newURI = "ipfs://updated-metadata"; const tx = await navigatorRegistry.connect(navigator1).setMetadataURI(newURI); await (0, chai_1.expect)(tx).to.emit(navigatorRegistry, "MetadataURIUpdated").withArgs(navigator1.address, newURI); (0, chai_1.expect)(await navigatorRegistry.getMetadataURI(navigator1.address)).to.equal(newURI); }); (0, mocha_1.it)("reverts if not navigator", async function () { await (0, chai_1.expect)(navigatorRegistry.connect(nonNavigator).setMetadataURI("ipfs://fail")).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered"); }); }); // ======================== 6. submitReport() ======================== // (0, mocha_1.describe)("submitReport()", function () { (0, mocha_1.it)("happy path: updates lastReportRound and lastReportURI, emits ReportSubmitted", async function () { const reportURI = "ipfs://report-1"; const currentRound = await xAllocationVoting.currentRoundId(); const tx = await navigatorRegistry.connect(navigator1).submitReport(reportURI); await (0, chai_1.expect)(tx) .to.emit(navigatorRegistry, "ReportSubmitted") .withArgs(navigator1.address, currentRound, reportURI); (0, chai_1.expect)(await navigatorRegistry.getLastReportRound(navigator1.address)).to.equal(currentRound); (0, chai_1.expect)(await navigatorRegistry.getLastReportURI(navigator1.address)).to.equal(reportURI); }); (0, mocha_1.it)("reverts if not navigator", async function () { await (0, chai_1.expect)(navigatorRegistry.connect(nonNavigator).submitReport("ipfs://fail")).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered"); }); (0, mocha_1.it)("multiple reports: latest values are correct", async function () { const report1 = "ipfs://report-1"; const report2 = "ipfs://report-2"; await navigatorRegistry.connect(navigator1).submitReport(report1); // Advance to next round const round1 = await xAllocationVoting.currentRoundId(); await (0, common_1.waitForRoundToEnd)(Number(round1)); await emissions.connect(minterAccount).distribute(); const round2 = await xAllocationVoting.currentRoundId(); await navigatorRegistry.connect(navigator1).submitReport(report2); (0, chai_1.expect)(await navigatorRegistry.getLastReportRound(navigator1.address)).to.equal(round2); (0, chai_1.expect)(await navigatorRegistry.getLastReportURI(navigator1.address)).to.equal(report2); }); }); // ======================== Citizen POV: Navigator Exit ======================== // (0, mocha_1.describe)("Citizen experience when navigator exits", function () { let citizen1; let vot3; (0, mocha_1.beforeEach)(async function () { citizen1 = otherAccounts[11]; vot3 = (await (0, deploy_1.getOrDeployContractInstances)({})).vot3; // Register navigator and delegate citizen await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); }); (0, mocha_1.it)("citizen's VOT3 is auto-unlocked as soon as navigator announces exit", async function () { // Before exit: VOT3 is locked (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(hardhat_1.ethers.parseEther("500")); await navigatorRegistry.connect(navigator1).announceExit(); // Lazy invalidation: delegation void immediately on announceExit (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.be.false; (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); // VOT3 auto-unlocked — full balance transferable const balance = await vot3.balanceOf(citizen1.address); await (0, chai_1.expect)(vot3.connect(citizen1).transfer(otherAccounts[14].address, balance)).to.not.be.reverted; }); (0, mocha_1.it)("decrements total delegated citizens when navigator announces exit", async function () { const beforeExitBlock = await hardhat_1.ethers.provider.getBlockNumber(); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(beforeExitBlock)).to.equal(1); await navigatorRegistry.connect(navigator1).announceExit(); const afterExitBlock = await hardhat_1.ethers.provider.getBlockNumber(); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(afterExitBlock)).to.equal(0); }); (0, mocha_1.it)("citizen's VOT3 is auto-unlocked after navigator announces exit (lazy invalidation)", async function () { await navigatorRegistry.connect(navigator1).announceExit(); // Lazy invalidation: isDelegated returns false, getDelegatedAmount returns 0 (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.be.false; (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); // VOT3 auto-unlocked — full balance transferable const balance = await vot3.balanceOf(citizen1.address); await (0, chai_1.expect)(vot3.connect(citizen1).transfer(otherAccounts[14].address, balance)).to.not.be.reverted; }); (0, mocha_1.it)("undelegate reverts with NotDelegated when navigator is dead (delegation void)", async function () { await navigatorRegistry.connect(navigator1).announceExit(); // Delegation is void — undelegate is not allowed await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).undelegate()).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); // View functions confirm delegation is void (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.be.false; (0, chai_1.expect)(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(hardhat_1.ethers.ZeroAddress); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); }); (0, mocha_1.it)("citizen can re-delegate to new navigator without calling undelegate (auto-clear)", async function () { const navigator2 = otherAccounts[12]; await b3tr.connect(owner).transfer(navigator2.address, hardhat_1.ethers.parseEther("50000")); await b3tr.connect(navigator2).approve(await navigatorRegistry.getAddress(), hardhat_1.ethers.parseEther("50000")); await navigatorRegistry.connect(navigator2).register(hardhat_1.ethers.parseEther("50000"), "ipfs://nav2"); // Exit navigator1 await navigatorRegistry.connect(navigator1).announceExit(); // Citizen directly delegates to new navigator — stale delegation auto-cleared await navigatorRegistry.connect(citizen1).delegate(navigator2.address, hardhat_1.ethers.parseEther("300")); (0, chai_1.expect)(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(navigator2.address); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(hardhat_1.ethers.parseEther("300")); }); (0, mocha_1.it)("no new citizens can delegate to exiting navigator", async function () { await navigatorRegistry.connect(navigator1).announceExit(); const newCitizen = otherAccounts[13]; await (0, common_1.getVot3Tokens)(newCitizen, "1000"); // New delegation rejected — navigator is exiting (exitAnnouncedRound > 0) await (0, chai_1.expect)(navigatorRegistry.connect(newCitizen).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"))).to.be.revertedWithCustomError(navigatorRegistry, "NavigatorCannotAcceptDelegations"); }); }); // ======================== Citizen POV: Navigator Deactivated ======================== // (0, mocha_1.describe)("Citizen experience when navigator is deactivated by governance", function () { let citizen1; let vot3; (0, mocha_1.beforeEach)(async function () { citizen1 = otherAccounts[11]; vot3 = (await (0, deploy_1.getOrDeployContractInstances)({})).vot3; await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); }); (0, mocha_1.it)("undelegate reverts with NotDelegated after navigator is deactivated (VOT3 auto-unlocked)", async function () { await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); // Delegation is void — undelegate is not allowed await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).undelegate()).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); // VOT3 is auto-unlocked — full balance transferable without undelegating (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.be.false; const balance = await vot3.balanceOf(citizen1.address); await (0, chai_1.expect)(vot3.connect(citizen1).transfer(otherAccounts[14].address, balance)).to.not.be.reverted; }); (0, mocha_1.it)("decrements total delegated citizens when navigator is deactivated", async function () { const beforeDeactivateBlock = await hardhat_1.ethers.provider.getBlockNumber(); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(beforeDeactivateBlock)).to.equal(1); await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); const afterDeactivateBlock = await hardhat_1.ethers.provider.getBlockNumber(); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(afterDeactivateBlock)).to.equal(0); }); (0, mocha_1.it)("new citizens cannot delegate to deactivated navigator", async function () { await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); const newCitizen = otherAccounts[13]; await (0, common_1.getVot3Tokens)(newCitizen, "1000"); await (0, chai_1.expect)(navigatorRegistry.connect(newCitizen).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"))).to.be.revertedWithCustomError(navigatorRegistry, "NavigatorCannotAcceptDelegations"); }); }); });