UNPKG

@vechain/vebetterdao-contracts

Version:

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

446 lines (445 loc) 34.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const chai_1 = require("chai"); const hardhat_1 = require("hardhat"); const deploy_1 = require("../helpers/deploy"); const common_1 = require("../helpers/common"); describe("NavigatorRegistry Delegation - @shard19b", function () { let navigatorRegistry; let b3tr; let vot3; let xAllocationVoting; let veBetterPassport; let owner; let navigator1; let citizen1; let citizen2; let citizen3; let otherAccount; const NAVIGATOR_STAKE = hardhat_1.ethers.parseEther("50000"); async function registerNavigator(nav, amount = NAVIGATOR_STAKE) { await b3tr.connect(owner).transfer(nav.address, amount); await b3tr.connect(nav).approve(await navigatorRegistry.getAddress(), amount); await navigatorRegistry.connect(nav).register(amount, "ipfs://navigator"); } beforeEach(async function () { const contracts = await (0, deploy_1.getOrDeployContractInstances)({ forceDeploy: true }); navigatorRegistry = contracts.navigatorRegistry; b3tr = contracts.b3tr; vot3 = contracts.vot3; xAllocationVoting = contracts.xAllocationVoting; veBetterPassport = contracts.veBetterPassport; owner = contracts.owner; navigator1 = contracts.otherAccounts[10]; citizen1 = contracts.otherAccounts[11]; citizen2 = contracts.otherAccounts[12]; citizen3 = contracts.otherAccounts[13]; otherAccount = contracts.otherAccounts[14]; // Mint B3TR to owner for navigator registration transfers await b3tr.connect(contracts.minterAccount).mint(owner.address, hardhat_1.ethers.parseEther("10000000")); // Ensure VOT3 supply exists (maxStake = vot3Supply * percentage / 10000) await (0, common_1.getVot3Tokens)(owner, "10000000"); // Register a navigator await registerNavigator(navigator1); }); // ======================== delegate() ======================== // describe("delegate()", function () { it("should delegate VOT3 to a navigator and update all state", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const amount = hardhat_1.ethers.parseEther("500"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); (0, chai_1.expect)(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(navigator1.address); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(amount); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegated(navigator1.address)).to.equal(amount); (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(true); }); it("should revert with ZeroDelegationAmount when amount is 0", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, 0)).to.be.revertedWithCustomError(navigatorRegistry, "ZeroDelegationAmount"); }); it("should revert with BelowMinimumDelegation when amount is below 1 VOT3", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const belowMin = hardhat_1.ethers.parseEther("0.5"); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, belowMin)).to.be.revertedWithCustomError(navigatorRegistry, "BelowMinimumDelegation"); }); it("should revert with AlreadyDelegated when citizen delegates again (use increaseDelegation instead)", async function () { await (0, common_1.getVot3Tokens)(citizen1, "2000"); const amount = hardhat_1.ethers.parseEther("500"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); // Second delegate() to same navigator should revert await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount)).to.be.revertedWithCustomError(navigatorRegistry, "AlreadyDelegated"); }); it("should revert with AlreadyDelegated when citizen delegates to a different navigator", async function () { await (0, common_1.getVot3Tokens)(citizen1, "2000"); const amount = hardhat_1.ethers.parseEther("500"); const navigator2 = otherAccount; await registerNavigator(navigator2); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator2.address, amount)).to.be.revertedWithCustomError(navigatorRegistry, "AlreadyDelegated"); }); it("should revert with NavigatorCannotAcceptDelegations when navigator is not registered", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(otherAccount.address, hardhat_1.ethers.parseEther("500"))).to.be.revertedWithCustomError(navigatorRegistry, "NavigatorCannotAcceptDelegations"); }); it("should revert when navigator is deactivated", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); // Deactivate navigator via governance await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"))).to.be.revertedWithCustomError(navigatorRegistry, "NavigatorCannotAcceptDelegations"); }); it("should revert with NavigatorCannotAcceptDelegations when navigator is exiting", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); // Bootstrap emissions so we have rounds await (0, common_1.bootstrapAndStartEmissions)(); // Navigator announces exit await navigatorRegistry.connect(navigator1).announceExit(); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500"))).to.be.revertedWithCustomError(navigatorRegistry, "NavigatorCannotAcceptDelegations"); }); it("should not emit DelegationRemoved when auto-clearing stale delegation to a dead navigator", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); // Deactivate navigator1 await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); // Register a second navigator const navigator2 = otherAccount; await registerNavigator(navigator2); // Re-delegate to navigator2 — should NOT emit DelegationRemoved for the stale delegation const tx = await navigatorRegistry.connect(citizen1).delegate(navigator2.address, hardhat_1.ethers.parseEther("500")); await (0, chai_1.expect)(tx).to.not.emit(navigatorRegistry, "DelegationRemoved"); await (0, chai_1.expect)(tx) .to.emit(navigatorRegistry, "DelegationCreated") .withArgs(citizen1.address, navigator2.address, hardhat_1.ethers.parseEther("500")); }); it("should revert with ExceedsNavigatorCapacity when delegation exceeds capacity", async function () { // Navigator staked 50K, capacity = 50K * 10 = 500K await (0, common_1.getVot3Tokens)(citizen1, "600000"); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("600000"))).to.be.revertedWithCustomError(navigatorRegistry, "ExceedsNavigatorCapacity"); }); it("should allow multiple citizens to delegate", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await (0, common_1.getVot3Tokens)(citizen2, "1000"); await (0, common_1.getVot3Tokens)(citizen3, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("100")); await navigatorRegistry.connect(citizen2).delegate(navigator1.address, hardhat_1.ethers.parseEther("200")); await navigatorRegistry.connect(citizen3).delegate(navigator1.address, hardhat_1.ethers.parseEther("300")); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegated(navigator1.address)).to.equal(hardhat_1.ethers.parseEther("100") + hardhat_1.ethers.parseEther("200") + hardhat_1.ethers.parseEther("300")); }); it("should emit DelegationCreated event", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const amount = hardhat_1.ethers.parseEther("500"); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount)) .to.emit(navigatorRegistry, "DelegationCreated") .withArgs(citizen1.address, navigator1.address, amount); }); it("should call disableAutoVotingFor when citizen delegates", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); // Verify auto-voting is disabled (default) — delegate should not revert even without auto-voting (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabled(citizen1.address)).to.equal(false); // Delegate — disableAutoVotingFor is called but is a no-op since auto-voting wasn't enabled await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); // Still disabled (0, chai_1.expect)(await xAllocationVoting.isUserAutoVotingEnabled(citizen1.address)).to.equal(false); }); }); // ======================== increaseDelegation() ======================== // describe("increaseDelegation()", function () { it("should increase delegation and emit DelegationIncreased", async function () { await (0, common_1.getVot3Tokens)(citizen1, "2000"); const initial = hardhat_1.ethers.parseEther("500"); const increase = hardhat_1.ethers.parseEther("300"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, initial); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).increaseDelegation(increase)) .to.emit(navigatorRegistry, "DelegationIncreased") .withArgs(citizen1.address, navigator1.address, increase, initial + increase); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(initial + increase); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegated(navigator1.address)).to.equal(initial + increase); }); it("should revert with NotDelegated when citizen is not delegated", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).increaseDelegation(hardhat_1.ethers.parseEther("100"))).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); it("should revert with ZeroDelegationAmount when amount is 0", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).increaseDelegation(0)).to.be.revertedWithCustomError(navigatorRegistry, "ZeroDelegationAmount"); }); it("should revert with ExceedsNavigatorCapacity when increase exceeds capacity", async function () { await (0, common_1.getVot3Tokens)(citizen1, "600000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("100")); // Navigator staked 50K, capacity = 500K. Already 100 delegated. Trying to add 500K should fail. await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).increaseDelegation(hardhat_1.ethers.parseEther("500000"))).to.be.revertedWithCustomError(navigatorRegistry, "ExceedsNavigatorCapacity"); }); }); // ======================== reduceDelegation() ======================== // describe("reduceDelegation()", function () { it("should partially reduce delegation and emit DelegationDecreased", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("200"))) .to.emit(navigatorRegistry, "DelegationDecreased") .withArgs(citizen1.address, navigator1.address, hardhat_1.ethers.parseEther("200"), hardhat_1.ethers.parseEther("300")); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(hardhat_1.ethers.parseEther("300")); }); it("should revert with BelowMinimumDelegation when partial reduction would leave between 0 and 1 VOT3", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("2")); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("1.1"))).to.be.revertedWithCustomError(navigatorRegistry, "BelowMinimumDelegation"); }); it("should allow partial reduction leaving exactly 1 VOT3", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("3")); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("2"))) .to.emit(navigatorRegistry, "DelegationDecreased") .withArgs(citizen1.address, navigator1.address, hardhat_1.ethers.parseEther("2"), hardhat_1.ethers.parseEther("1")); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(hardhat_1.ethers.parseEther("1")); }); it("should fully undelegate when reducing to zero and emit DelegationRemoved", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("500"))) .to.emit(navigatorRegistry, "DelegationRemoved") .withArgs(citizen1.address, navigator1.address, hardhat_1.ethers.parseEther("500")); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); (0, chai_1.expect)(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(hardhat_1.ethers.ZeroAddress); (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(false); }); it("should revert with InsufficientDelegation when reducing more than delegated", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("501"))).to.be.revertedWithCustomError(navigatorRegistry, "InsufficientDelegation"); }); it("should revert with ZeroDelegationAmount when amount is 0", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).reduceDelegation(0)).to.be.revertedWithCustomError(navigatorRegistry, "ZeroDelegationAmount"); }); it("should revert with NotDelegated when citizen is not delegated", async function () { await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("100"))).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); it("should revert with NotDelegated when navigator is dead", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("100"))).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); }); // ======================== undelegate() ======================== // describe("undelegate()", function () { it("should fully undelegate and clear all state", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await navigatorRegistry.connect(citizen1).undelegate(); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); (0, chai_1.expect)(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(hardhat_1.ethers.ZeroAddress); (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(false); }); it("should revert with NotDelegated when citizen is not delegated", async function () { await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).undelegate()).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); it("should revert with NotDelegated when navigator is dead", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).undelegate()).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); it("should correctly maintain citizen array after middle undelegate", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await (0, common_1.getVot3Tokens)(citizen2, "1000"); await (0, common_1.getVot3Tokens)(citizen3, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("100")); await navigatorRegistry.connect(citizen2).delegate(navigator1.address, hardhat_1.ethers.parseEther("100")); await navigatorRegistry.connect(citizen3).delegate(navigator1.address, hardhat_1.ethers.parseEther("100")); // Undelegate the middle citizen await navigatorRegistry.connect(citizen2).undelegate(); (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(true); (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen2.address)).to.equal(false); (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen3.address)).to.equal(true); }); }); // ======================== Checkpoints ======================== // describe("Checkpoints", function () { it("should return 0 for delegation amount before delegation block", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const blockBefore = await hardhat_1.ethers.provider.getBlockNumber(); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await (0, common_1.waitForNextBlock)(); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen1.address, blockBefore)).to.equal(0); }); it("should return delegated amount at delegation block", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const amount = hardhat_1.ethers.parseEther("500"); const tx = await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); const receipt = await tx.wait(); const delegationBlock = receipt.blockNumber; await (0, common_1.moveBlocks)(3); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen1.address, delegationBlock)).to.equal(amount); }); it("should preserve old amount at old block after reduce", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const initialAmount = hardhat_1.ethers.parseEther("500"); const tx = await navigatorRegistry.connect(citizen1).delegate(navigator1.address, initialAmount); const receipt = await tx.wait(); const delegationBlock = receipt.blockNumber; await (0, common_1.moveBlocks)(3); // Reduce delegation await navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("200")); await (0, common_1.moveBlocks)(2); // Old block should still show old amount (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen1.address, delegationBlock)).to.equal(initialAmount); // Current amount should be reduced (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(hardhat_1.ethers.parseEther("300")); }); it("should checkpoint total delegated citizens count across delegation lifecycle", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await (0, common_1.getVot3Tokens)(citizen2, "1000"); const blockBefore = await hardhat_1.ethers.provider.getBlockNumber(); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); const blockAfterFirstDelegate = await hardhat_1.ethers.provider.getBlockNumber(); await navigatorRegistry.connect(citizen2).delegate(navigator1.address, hardhat_1.ethers.parseEther("200")); const blockAfterSecondDelegate = await hardhat_1.ethers.provider.getBlockNumber(); // Partial reduction keeps citizen delegated, so citizen-count checkpoint must remain unchanged await navigatorRegistry.connect(citizen1).reduceDelegation(hardhat_1.ethers.parseEther("100")); const blockAfterPartialReduce = await hardhat_1.ethers.provider.getBlockNumber(); await navigatorRegistry.connect(citizen1).undelegate(); const blockAfterUndelegate = await hardhat_1.ethers.provider.getBlockNumber(); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockBefore)).to.equal(0); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockAfterFirstDelegate)).to.equal(1); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockAfterSecondDelegate)).to.equal(2); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockAfterPartialReduce)).to.equal(2); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockAfterUndelegate)).to.equal(1); }); }); // ======================== VOT3 Lock ======================== // describe("VOT3 Lock", function () { it("should allow transfer of unlocked balance", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); // Transfer 500 (unlocked portion) should succeed await (0, chai_1.expect)(vot3.connect(citizen1).transfer(otherAccount.address, hardhat_1.ethers.parseEther("500"))).to.not.be.reverted; }); it("should revert transfer that dips into locked balance", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); // Transfer 501 should fail — would go below locked 500 await (0, chai_1.expect)(vot3.connect(citizen1).transfer(otherAccount.address, hardhat_1.ethers.parseEther("501"))).to.be.revertedWith("VOT3: transfer exceeds unlocked balance"); }); it("should revert convertToB3TR that dips into locked balance", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); // convertToB3TR(501) should fail await (0, chai_1.expect)(vot3.connect(citizen1).convertToB3TR(hardhat_1.ethers.parseEther("501"))).to.be.revertedWith("VOT3: transfer exceeds unlocked balance"); }); it("should allow full transfer after undelegate", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); await navigatorRegistry.connect(citizen1).undelegate(); // Full balance should be transferable now const balance = await vot3.balanceOf(citizen1.address); await (0, chai_1.expect)(vot3.connect(citizen1).transfer(otherAccount.address, balance)).to.not.be.reverted; }); it("should return correct getNavigatorLockedAmount on VOT3", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const amount = hardhat_1.ethers.parseEther("500"); (0, chai_1.expect)(await vot3.getNavigatorLockedAmount(citizen1.address)).to.equal(0); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); (0, chai_1.expect)(await vot3.getNavigatorLockedAmount(citizen1.address)).to.equal(amount); await navigatorRegistry.connect(citizen1).undelegate(); (0, chai_1.expect)(await vot3.getNavigatorLockedAmount(citizen1.address)).to.equal(0); }); }); // ======================== Unlocked Balance Guard ======================== // describe("Unlocked balance guard", function () { it("should revert delegate() with InsufficientUnlockedBalance when amount > balance", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const overBalance = hardhat_1.ethers.parseEther("1001"); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, overBalance)).to.be.revertedWithCustomError(navigatorRegistry, "InsufficientUnlockedBalance"); }); it("should revert increaseDelegation() with InsufficientUnlockedBalance when total would exceed balance", async function () { await (0, common_1.getVot3Tokens)(citizen1, "3000"); const full = hardhat_1.ethers.parseEther("3000"); // First delegation: full balance await navigatorRegistry.connect(citizen1).delegate(navigator1.address, full); // Reproduces reported bug: delegate full balance, then try to increase by another full balance. // Pre-fix this would succeed and end at 6000 delegated for a 3000 balance citizen. await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).increaseDelegation(full)).to.be.revertedWithCustomError(navigatorRegistry, "InsufficientUnlockedBalance"); // State unchanged (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(full); }); it("should allow delegate up to exactly balance", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const full = hardhat_1.ethers.parseEther("1000"); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator1.address, full)).to.not.be.reverted; (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(full); }); it("should allow incremental increases up to balance", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const half = hardhat_1.ethers.parseEther("500"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, half); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).increaseDelegation(half)).to.not.be.reverted; (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(hardhat_1.ethers.parseEther("1000")); }); it("switch flow (undelegate + delegate) still works for full balance", async function () { // Register a second navigator on a fresh signer const signers = await hardhat_1.ethers.getSigners(); const navigator2 = signers[19]; await registerNavigator(navigator2); await (0, common_1.getVot3Tokens)(citizen1, "1000"); const full = hardhat_1.ethers.parseEther("1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, full); // VeChain switch is a single multi-clause tx (undelegate then delegate). On hardhat we // sequence as two txs — same end state. Post-undelegate, unlockedBalance == balanceOf, // so re-delegating the full balance to a new nav passes the guard. await navigatorRegistry.connect(citizen1).undelegate(); await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).delegate(navigator2.address, full)).to.not.be.reverted; (0, chai_1.expect)(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(navigator2.address); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(full); }); }); // ======================== Over-delegation Corrector ======================== // describe("correctOverDelegations()", function () { /** * Helper: forcibly induce an over-delegated state by upgrading "back" to a buggy state. * Since the V2 guard prevents creating over-delegation, we simulate it by transferring VOT3 * away from the citizen using the admin's mint/transfer route. But VOT3's transfer lock * blocks that. So instead we mint extra VOT3 to the citizen, delegate the inflated amount, * then convert the extra back to B3TR is also blocked. * * The cleanest reproduction: delegate full balance via the now-correct guard, then have a * dead-nav scenario where balance drops via convertToB3TR after lazy-invalidation, then * re-delegate via auto-clear, etc. — but that would test contrived flows. * * For functional coverage of the corrector logic, we directly test idempotency on a * non-over-delegated citizen and the no-op paths. The reverse-engineering required to * synthesize over-delegation post-fix would itself rely on bypassing the guard. */ it("should be a no-op for citizens with no delegation", async function () { await navigatorRegistry.correctOverDelegations([citizen1.address]); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); (0, chai_1.expect)(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(false); }); it("should be a no-op for citizens at or below their balance (idempotent)", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); const amount = hardhat_1.ethers.parseEther("500"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); const totalBefore = await navigatorRegistry.getTotalDelegated(navigator1.address); await navigatorRegistry.correctOverDelegations([citizen1.address]); // Re-run to assert idempotency await navigatorRegistry.correctOverDelegations([citizen1.address]); (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(amount); (0, chai_1.expect)(await navigatorRegistry.getTotalDelegated(navigator1.address)).to.equal(totalBefore); }); it("should require DEFAULT_ADMIN_ROLE", async function () { await (0, chai_1.expect)(navigatorRegistry.connect(citizen1).correctOverDelegations([citizen1.address])).to.be.reverted; }); it("should skip citizens whose navigator is dead (lazy-invalidated)", async function () { await (0, common_1.getVot3Tokens)(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, hardhat_1.ethers.parseEther("500")); // Deactivate navigator (governance-driven major slash with 0%) await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); // Lazy invalidation: getDelegatedAmount returns 0 (0, chai_1.expect)(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); // Corrector skips dead-nav citizens — no events, no state change await (0, chai_1.expect)(navigatorRegistry.correctOverDelegations([citizen1.address])).to.not.emit(navigatorRegistry, "DelegationDecreased"); }); }); });