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