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