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