UNPKG

@vechain/vebetterdao-contracts

Version:

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

444 lines (443 loc) 32.3 kB
import { expect } from "chai"; import { ethers } from "hardhat"; import { getOrDeployContractInstances } from "../helpers/deploy"; import { getVot3Tokens, bootstrapAndStartEmissions, moveBlocks, waitForNextBlock } from "../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 = 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 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, ethers.parseEther("10000000")); // Ensure VOT3 supply exists (maxStake = vot3Supply * percentage / 10000) await 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 getVot3Tokens(citizen1, "1000"); const amount = ethers.parseEther("500"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); expect(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(navigator1.address); expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(amount); expect(await navigatorRegistry.getTotalDelegated(navigator1.address)).to.equal(amount); expect(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(true); }); it("should revert with ZeroDelegationAmount when amount is 0", async function () { await getVot3Tokens(citizen1, "1000"); await 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 getVot3Tokens(citizen1, "1000"); const belowMin = ethers.parseEther("0.5"); await 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 getVot3Tokens(citizen1, "2000"); const amount = ethers.parseEther("500"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); // Second delegate() to same navigator should revert await 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 getVot3Tokens(citizen1, "2000"); const amount = ethers.parseEther("500"); const navigator2 = otherAccount; await registerNavigator(navigator2); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); await 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 getVot3Tokens(citizen1, "1000"); await expect(navigatorRegistry.connect(citizen1).delegate(otherAccount.address, ethers.parseEther("500"))).to.be.revertedWithCustomError(navigatorRegistry, "NavigatorCannotAcceptDelegations"); }); it("should revert when navigator is deactivated", async function () { await getVot3Tokens(citizen1, "1000"); // Deactivate navigator via governance await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await expect(navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500"))).to.be.revertedWithCustomError(navigatorRegistry, "NavigatorCannotAcceptDelegations"); }); it("should revert with NavigatorCannotAcceptDelegations when navigator is exiting", async function () { await getVot3Tokens(citizen1, "1000"); // Bootstrap emissions so we have rounds await bootstrapAndStartEmissions(); // Navigator announces exit await navigatorRegistry.connect(navigator1).announceExit(); await expect(navigatorRegistry.connect(citizen1).delegate(navigator1.address, 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 getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, 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, ethers.parseEther("500")); await expect(tx).to.not.emit(navigatorRegistry, "DelegationRemoved"); await expect(tx) .to.emit(navigatorRegistry, "DelegationCreated") .withArgs(citizen1.address, navigator2.address, ethers.parseEther("500")); }); it("should revert with ExceedsNavigatorCapacity when delegation exceeds capacity", async function () { // Navigator staked 50K, capacity = 50K * 10 = 500K await getVot3Tokens(citizen1, "600000"); await expect(navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("600000"))).to.be.revertedWithCustomError(navigatorRegistry, "ExceedsNavigatorCapacity"); }); it("should allow multiple citizens to delegate", async function () { await getVot3Tokens(citizen1, "1000"); await getVot3Tokens(citizen2, "1000"); await getVot3Tokens(citizen3, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("100")); await navigatorRegistry.connect(citizen2).delegate(navigator1.address, ethers.parseEther("200")); await navigatorRegistry.connect(citizen3).delegate(navigator1.address, ethers.parseEther("300")); expect(await navigatorRegistry.getTotalDelegated(navigator1.address)).to.equal(ethers.parseEther("100") + ethers.parseEther("200") + ethers.parseEther("300")); }); it("should emit DelegationCreated event", async function () { await getVot3Tokens(citizen1, "1000"); const amount = ethers.parseEther("500"); await 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 getVot3Tokens(citizen1, "1000"); // Verify auto-voting is disabled (default) — delegate should not revert even without auto-voting 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, ethers.parseEther("500")); // Still disabled expect(await xAllocationVoting.isUserAutoVotingEnabled(citizen1.address)).to.equal(false); }); }); // ======================== increaseDelegation() ======================== // describe("increaseDelegation()", function () { it("should increase delegation and emit DelegationIncreased", async function () { await getVot3Tokens(citizen1, "2000"); const initial = ethers.parseEther("500"); const increase = ethers.parseEther("300"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, initial); await expect(navigatorRegistry.connect(citizen1).increaseDelegation(increase)) .to.emit(navigatorRegistry, "DelegationIncreased") .withArgs(citizen1.address, navigator1.address, increase, initial + increase); expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(initial + increase); expect(await navigatorRegistry.getTotalDelegated(navigator1.address)).to.equal(initial + increase); }); it("should revert with NotDelegated when citizen is not delegated", async function () { await getVot3Tokens(citizen1, "1000"); await expect(navigatorRegistry.connect(citizen1).increaseDelegation(ethers.parseEther("100"))).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); it("should revert with ZeroDelegationAmount when amount is 0", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await expect(navigatorRegistry.connect(citizen1).increaseDelegation(0)).to.be.revertedWithCustomError(navigatorRegistry, "ZeroDelegationAmount"); }); it("should revert with ExceedsNavigatorCapacity when increase exceeds capacity", async function () { await getVot3Tokens(citizen1, "600000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("100")); // Navigator staked 50K, capacity = 500K. Already 100 delegated. Trying to add 500K should fail. await expect(navigatorRegistry.connect(citizen1).increaseDelegation(ethers.parseEther("500000"))).to.be.revertedWithCustomError(navigatorRegistry, "ExceedsNavigatorCapacity"); }); }); // ======================== reduceDelegation() ======================== // describe("reduceDelegation()", function () { it("should partially reduce delegation and emit DelegationDecreased", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await expect(navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("200"))) .to.emit(navigatorRegistry, "DelegationDecreased") .withArgs(citizen1.address, navigator1.address, ethers.parseEther("200"), ethers.parseEther("300")); expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(ethers.parseEther("300")); }); it("should revert with BelowMinimumDelegation when partial reduction would leave between 0 and 1 VOT3", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("2")); await expect(navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("1.1"))).to.be.revertedWithCustomError(navigatorRegistry, "BelowMinimumDelegation"); }); it("should allow partial reduction leaving exactly 1 VOT3", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("3")); await expect(navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("2"))) .to.emit(navigatorRegistry, "DelegationDecreased") .withArgs(citizen1.address, navigator1.address, ethers.parseEther("2"), ethers.parseEther("1")); expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(ethers.parseEther("1")); }); it("should fully undelegate when reducing to zero and emit DelegationRemoved", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await expect(navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("500"))) .to.emit(navigatorRegistry, "DelegationRemoved") .withArgs(citizen1.address, navigator1.address, ethers.parseEther("500")); expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); expect(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(ethers.ZeroAddress); expect(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(false); }); it("should revert with InsufficientDelegation when reducing more than delegated", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await expect(navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("501"))).to.be.revertedWithCustomError(navigatorRegistry, "InsufficientDelegation"); }); it("should revert with ZeroDelegationAmount when amount is 0", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await expect(navigatorRegistry.connect(citizen1).reduceDelegation(0)).to.be.revertedWithCustomError(navigatorRegistry, "ZeroDelegationAmount"); }); it("should revert with NotDelegated when citizen is not delegated", async function () { await expect(navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("100"))).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); it("should revert with NotDelegated when navigator is dead", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await expect(navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("100"))).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); }); // ======================== undelegate() ======================== // describe("undelegate()", function () { it("should fully undelegate and clear all state", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await navigatorRegistry.connect(citizen1).undelegate(); expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); expect(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(ethers.ZeroAddress); expect(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(false); }); it("should revert with NotDelegated when citizen is not delegated", async function () { await expect(navigatorRegistry.connect(citizen1).undelegate()).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); it("should revert with NotDelegated when navigator is dead", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await navigatorRegistry.connect(owner).deactivateNavigator(navigator1.address, 0, false); await expect(navigatorRegistry.connect(citizen1).undelegate()).to.be.revertedWithCustomError(navigatorRegistry, "NotDelegated"); }); it("should correctly maintain citizen array after middle undelegate", async function () { await getVot3Tokens(citizen1, "1000"); await getVot3Tokens(citizen2, "1000"); await getVot3Tokens(citizen3, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("100")); await navigatorRegistry.connect(citizen2).delegate(navigator1.address, ethers.parseEther("100")); await navigatorRegistry.connect(citizen3).delegate(navigator1.address, ethers.parseEther("100")); // Undelegate the middle citizen await navigatorRegistry.connect(citizen2).undelegate(); expect(await navigatorRegistry.isDelegated(citizen1.address)).to.equal(true); expect(await navigatorRegistry.isDelegated(citizen2.address)).to.equal(false); 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 getVot3Tokens(citizen1, "1000"); const blockBefore = await ethers.provider.getBlockNumber(); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await waitForNextBlock(); expect(await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen1.address, blockBefore)).to.equal(0); }); it("should return delegated amount at delegation block", async function () { await getVot3Tokens(citizen1, "1000"); const amount = ethers.parseEther("500"); const tx = await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); const receipt = await tx.wait(); const delegationBlock = receipt.blockNumber; await moveBlocks(3); expect(await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen1.address, delegationBlock)).to.equal(amount); }); it("should preserve old amount at old block after reduce", async function () { await getVot3Tokens(citizen1, "1000"); const initialAmount = ethers.parseEther("500"); const tx = await navigatorRegistry.connect(citizen1).delegate(navigator1.address, initialAmount); const receipt = await tx.wait(); const delegationBlock = receipt.blockNumber; await moveBlocks(3); // Reduce delegation await navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("200")); await moveBlocks(2); // Old block should still show old amount expect(await navigatorRegistry.getDelegatedAmountAtTimepoint(citizen1.address, delegationBlock)).to.equal(initialAmount); // Current amount should be reduced expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(ethers.parseEther("300")); }); it("should checkpoint total delegated citizens count across delegation lifecycle", async function () { await getVot3Tokens(citizen1, "1000"); await getVot3Tokens(citizen2, "1000"); const blockBefore = await ethers.provider.getBlockNumber(); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); const blockAfterFirstDelegate = await ethers.provider.getBlockNumber(); await navigatorRegistry.connect(citizen2).delegate(navigator1.address, ethers.parseEther("200")); const blockAfterSecondDelegate = await ethers.provider.getBlockNumber(); // Partial reduction keeps citizen delegated, so citizen-count checkpoint must remain unchanged await navigatorRegistry.connect(citizen1).reduceDelegation(ethers.parseEther("100")); const blockAfterPartialReduce = await ethers.provider.getBlockNumber(); await navigatorRegistry.connect(citizen1).undelegate(); const blockAfterUndelegate = await ethers.provider.getBlockNumber(); expect(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockBefore)).to.equal(0); expect(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockAfterFirstDelegate)).to.equal(1); expect(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockAfterSecondDelegate)).to.equal(2); expect(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockAfterPartialReduce)).to.equal(2); expect(await navigatorRegistry.getTotalDelegatedCitizensAtTimepoint(blockAfterUndelegate)).to.equal(1); }); }); // ======================== VOT3 Lock ======================== // describe("VOT3 Lock", function () { it("should allow transfer of unlocked balance", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); // Transfer 500 (unlocked portion) should succeed await expect(vot3.connect(citizen1).transfer(otherAccount.address, ethers.parseEther("500"))).to.not.be.reverted; }); it("should revert transfer that dips into locked balance", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); // Transfer 501 should fail — would go below locked 500 await expect(vot3.connect(citizen1).transfer(otherAccount.address, ethers.parseEther("501"))).to.be.revertedWith("VOT3: transfer exceeds unlocked balance"); }); it("should revert convertToB3TR that dips into locked balance", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); // convertToB3TR(501) should fail await expect(vot3.connect(citizen1).convertToB3TR(ethers.parseEther("501"))).to.be.revertedWith("VOT3: transfer exceeds unlocked balance"); }); it("should allow full transfer after undelegate", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, ethers.parseEther("500")); await navigatorRegistry.connect(citizen1).undelegate(); // Full balance should be transferable now const balance = await vot3.balanceOf(citizen1.address); await expect(vot3.connect(citizen1).transfer(otherAccount.address, balance)).to.not.be.reverted; }); it("should return correct getNavigatorLockedAmount on VOT3", async function () { await getVot3Tokens(citizen1, "1000"); const amount = ethers.parseEther("500"); expect(await vot3.getNavigatorLockedAmount(citizen1.address)).to.equal(0); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, amount); expect(await vot3.getNavigatorLockedAmount(citizen1.address)).to.equal(amount); await navigatorRegistry.connect(citizen1).undelegate(); 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 getVot3Tokens(citizen1, "1000"); const overBalance = ethers.parseEther("1001"); await 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 getVot3Tokens(citizen1, "3000"); const full = 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 expect(navigatorRegistry.connect(citizen1).increaseDelegation(full)).to.be.revertedWithCustomError(navigatorRegistry, "InsufficientUnlockedBalance"); // State unchanged expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(full); }); it("should allow delegate up to exactly balance", async function () { await getVot3Tokens(citizen1, "1000"); const full = ethers.parseEther("1000"); await expect(navigatorRegistry.connect(citizen1).delegate(navigator1.address, full)).to.not.be.reverted; expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(full); }); it("should allow incremental increases up to balance", async function () { await getVot3Tokens(citizen1, "1000"); const half = ethers.parseEther("500"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, half); await expect(navigatorRegistry.connect(citizen1).increaseDelegation(half)).to.not.be.reverted; expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(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 ethers.getSigners(); const navigator2 = signers[19]; await registerNavigator(navigator2); await getVot3Tokens(citizen1, "1000"); const full = 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 expect(navigatorRegistry.connect(citizen1).delegate(navigator2.address, full)).to.not.be.reverted; expect(await navigatorRegistry.getNavigator(citizen1.address)).to.equal(navigator2.address); 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]); expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); 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 getVot3Tokens(citizen1, "1000"); const amount = 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]); expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(amount); expect(await navigatorRegistry.getTotalDelegated(navigator1.address)).to.equal(totalBefore); }); it("should require DEFAULT_ADMIN_ROLE", async function () { await expect(navigatorRegistry.connect(citizen1).correctOverDelegations([citizen1.address])).to.be.reverted; }); it("should skip citizens whose navigator is dead (lazy-invalidated)", async function () { await getVot3Tokens(citizen1, "1000"); await navigatorRegistry.connect(citizen1).delegate(navigator1.address, 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 expect(await navigatorRegistry.getDelegatedAmount(citizen1.address)).to.equal(0); // Corrector skips dead-nav citizens — no events, no state change await expect(navigatorRegistry.correctOverDelegations([citizen1.address])).to.not.emit(navigatorRegistry, "DelegationDecreased"); }); }); });