@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
250 lines (249 loc) • 16.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const hardhat_1 = require("hardhat");
const chai_1 = require("chai");
const mocha_1 = require("mocha");
const local_1 = require("@repo/config/contracts/envs/local");
const deploy_1 = require("../helpers/deploy");
const common_1 = require("../helpers/common");
(0, mocha_1.describe)("NavigatorRegistry Fees - @shard19d", function () {
let navigatorRegistry;
let b3tr;
let voterRewards;
let emissions;
let xAllocationVoting;
let owner;
let otherAccounts;
let minterAccount;
const config = (0, local_1.createLocalConfig)();
const STAKE_AMOUNT = hardhat_1.ethers.parseEther("50000");
const METADATA_URI = "ipfs://navigator-metadata";
const FEE_AMOUNT = hardhat_1.ethers.parseEther("100");
// Helper: fund account with B3TR and approve NavigatorRegistry
const fundAndApprove = async (account, amount) => {
await b3tr.connect(owner).transfer(account.address, amount);
const registryAddress = await navigatorRegistry.getAddress();
await b3tr.connect(account).approve(registryAddress, amount);
};
// Helper: register a navigator with default stake
const registerNavigator = async (account, amount = STAKE_AMOUNT, uri = METADATA_URI) => {
await fundAndApprove(account, amount);
await navigatorRegistry.connect(account).register(amount, uri);
};
// Helper: impersonate VoterRewards contract and deposit a fee
const depositFeeViaImpersonation = async (navigator, roundId, amount) => {
const voterRewardsAddress = await voterRewards.getAddress();
await hardhat_1.ethers.provider.send("hardhat_impersonateAccount", [voterRewardsAddress]);
await hardhat_1.ethers.provider.send("hardhat_setBalance", [voterRewardsAddress, "0x" + (10n ** 18n).toString(16)]);
const voterRewardsSigner = await hardhat_1.ethers.getSigner(voterRewardsAddress);
// Transfer B3TR to NavigatorRegistry so it can pay out later
const registryAddress = await navigatorRegistry.getAddress();
await b3tr.connect(owner).transfer(registryAddress, amount);
// Deposit fee
await navigatorRegistry.connect(voterRewardsSigner).depositNavigatorFee(navigator, roundId, amount);
await hardhat_1.ethers.provider.send("hardhat_stopImpersonatingAccount", [voterRewardsAddress]);
};
// Helper: advance N allocation rounds (requires emissions running)
const advanceRounds = async (count) => {
for (let i = 0; i < count; i++) {
const roundId = await xAllocationVoting.currentRoundId();
await (0, common_1.waitForRoundToEnd)(Number(roundId));
await emissions.distribute();
}
};
(0, mocha_1.beforeEach)(async function () {
const deployment = await (0, deploy_1.getOrDeployContractInstances)({ forceDeploy: true });
if (!deployment)
throw new Error("Failed to deploy contracts");
navigatorRegistry = deployment.navigatorRegistry;
b3tr = deployment.b3tr;
voterRewards = deployment.voterRewards;
emissions = deployment.emissions;
xAllocationVoting = deployment.xAllocationVoting;
owner = deployment.owner;
otherAccounts = deployment.otherAccounts;
minterAccount = deployment.minterAccount;
// Mint B3TR to owner for navigator registration transfers
await b3tr.connect(minterAccount).mint(owner.address, hardhat_1.ethers.parseEther("10000000"));
// Create VOT3 supply (max stake = 1% of VOT3 supply, need >= 5M VOT3 for 50k stake)
await (0, common_1.getVot3Tokens)(otherAccounts[15], "10000000");
});
// ======================== 1. depositNavigatorFee ======================== //
(0, mocha_1.describe)("depositNavigatorFee", function () {
(0, mocha_1.it)("should deposit fee and record it in getRoundFee", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
await depositFeeViaImpersonation(navigator.address, roundId, FEE_AMOUNT);
(0, chai_1.expect)(await navigatorRegistry.getRoundFee(navigator.address, roundId)).to.equal(FEE_AMOUNT);
});
(0, mocha_1.it)("should revert when called by non-VoterRewards address", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
await (0, chai_1.expect)(navigatorRegistry.connect(owner).depositNavigatorFee(navigator.address, roundId, FEE_AMOUNT)).to.be.revertedWithCustomError(navigatorRegistry, "UnauthorizedCaller");
});
(0, mocha_1.it)("should accumulate multiple deposits in the same round", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
const firstDeposit = hardhat_1.ethers.parseEther("50");
const secondDeposit = hardhat_1.ethers.parseEther("75");
await depositFeeViaImpersonation(navigator.address, roundId, firstDeposit);
await depositFeeViaImpersonation(navigator.address, roundId, secondDeposit);
(0, chai_1.expect)(await navigatorRegistry.getRoundFee(navigator.address, roundId)).to.equal(firstDeposit + secondDeposit);
});
(0, mocha_1.it)("should emit FeeDeposited event", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
const voterRewardsAddress = await voterRewards.getAddress();
await hardhat_1.ethers.provider.send("hardhat_impersonateAccount", [voterRewardsAddress]);
await hardhat_1.ethers.provider.send("hardhat_setBalance", [voterRewardsAddress, "0x" + (10n ** 18n).toString(16)]);
const voterRewardsSigner = await hardhat_1.ethers.getSigner(voterRewardsAddress);
const registryAddress = await navigatorRegistry.getAddress();
await b3tr.connect(owner).transfer(registryAddress, FEE_AMOUNT);
await (0, chai_1.expect)(navigatorRegistry.connect(voterRewardsSigner).depositNavigatorFee(navigator.address, roundId, FEE_AMOUNT))
.to.emit(navigatorRegistry, "FeeDeposited")
.withArgs(navigator.address, roundId, FEE_AMOUNT);
await hardhat_1.ethers.provider.send("hardhat_stopImpersonatingAccount", [voterRewardsAddress]);
});
});
// ======================== 2. claimFee ======================== //
(0, mocha_1.describe)("claimFee", function () {
(0, mocha_1.it)("should allow claiming after feeLockPeriod rounds", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
await depositFeeViaImpersonation(navigator.address, depositRoundId, FEE_AMOUNT);
// Advance feeLockPeriod rounds (default 4)
const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod());
await advanceRounds(lockPeriod);
const balanceBefore = await b3tr.balanceOf(navigator.address);
await navigatorRegistry.connect(navigator).claimFee(depositRoundId);
const balanceAfter = await b3tr.balanceOf(navigator.address);
(0, chai_1.expect)(balanceAfter - balanceBefore).to.equal(FEE_AMOUNT);
});
(0, mocha_1.it)("should revert when fees are still locked", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
await depositFeeViaImpersonation(navigator.address, depositRoundId, FEE_AMOUNT);
// Advance only 2 rounds (less than feeLockPeriod of 4)
await advanceRounds(2);
await (0, chai_1.expect)(navigatorRegistry.connect(navigator).claimFee(depositRoundId)).to.be.revertedWithCustomError(navigatorRegistry, "FeesStillLocked");
});
(0, mocha_1.it)("should revert when no fees exist for the round", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const roundId = await xAllocationVoting.currentRoundId();
// Advance past lock period with no deposited fees
const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod());
await advanceRounds(lockPeriod);
await (0, chai_1.expect)(navigatorRegistry.connect(navigator).claimFee(roundId)).to.be.revertedWithCustomError(navigatorRegistry, "NoFeesToClaim");
});
(0, mocha_1.it)("should revert when fees have been forfeited via deactivateNavigator with slashFees=true", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
await depositFeeViaImpersonation(navigator.address, depositRoundId, FEE_AMOUNT);
// Governance deactivates navigator with fee forfeiture
await navigatorRegistry.connect(owner).deactivateNavigator(navigator.address, 0, true);
// Advance past lock period
const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod());
await advanceRounds(lockPeriod);
// claimFee requires onlyNavigator, but navigator is deactivated => NotRegistered
await (0, chai_1.expect)(navigatorRegistry.connect(navigator).claimFee(depositRoundId)).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered");
});
(0, mocha_1.it)("should revert on double claim", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
await depositFeeViaImpersonation(navigator.address, depositRoundId, FEE_AMOUNT);
const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod());
await advanceRounds(lockPeriod);
// First claim succeeds
await navigatorRegistry.connect(navigator).claimFee(depositRoundId);
// Second claim reverts
await (0, chai_1.expect)(navigatorRegistry.connect(navigator).claimFee(depositRoundId)).to.be.revertedWithCustomError(navigatorRegistry, "NoFeesToClaim");
});
(0, mocha_1.it)("should emit FeeClaimed event", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
await depositFeeViaImpersonation(navigator.address, depositRoundId, FEE_AMOUNT);
const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod());
await advanceRounds(lockPeriod);
await (0, chai_1.expect)(navigatorRegistry.connect(navigator).claimFee(depositRoundId))
.to.emit(navigatorRegistry, "FeeClaimed")
.withArgs(navigator.address, depositRoundId, FEE_AMOUNT);
});
(0, mocha_1.it)("should clear round fee after successful claim", async function () {
const navigator = otherAccounts[10];
await registerNavigator(navigator);
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
await depositFeeViaImpersonation(navigator.address, depositRoundId, FEE_AMOUNT);
const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod());
await advanceRounds(lockPeriod);
await navigatorRegistry.connect(navigator).claimFee(depositRoundId);
(0, chai_1.expect)(await navigatorRegistry.getRoundFee(navigator.address, depositRoundId)).to.equal(0n);
});
});
// ======================== 3. View Functions ======================== //
(0, mocha_1.describe)("View functions", function () {
(0, mocha_1.it)("getFeeLockPeriod returns configured value", async function () {
(0, chai_1.expect)(await navigatorRegistry.getFeeLockPeriod()).to.equal(config.NAVIGATOR_FEE_LOCK_PERIOD);
});
(0, mocha_1.it)("getFeePercentage returns configured value", async function () {
(0, chai_1.expect)(await navigatorRegistry.getFeePercentage()).to.equal(config.NAVIGATOR_FEE_PERCENTAGE);
});
(0, mocha_1.it)("isRoundFeeUnlocked returns false when still locked", async function () {
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
// Advance 2 rounds (less than lock period of 4)
await advanceRounds(2);
(0, chai_1.expect)(await navigatorRegistry.isRoundFeeUnlocked(depositRoundId)).to.equal(false);
});
(0, mocha_1.it)("isRoundFeeUnlocked returns true after lock period", async function () {
await (0, common_1.bootstrapAndStartEmissions)();
const depositRoundId = await xAllocationVoting.currentRoundId();
const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod());
await advanceRounds(lockPeriod);
(0, chai_1.expect)(await navigatorRegistry.isRoundFeeUnlocked(depositRoundId)).to.equal(true);
});
(0, mocha_1.it)("getRoundFee returns 0 for rounds with no deposits", async function () {
const navigator = otherAccounts[10];
(0, chai_1.expect)(await navigatorRegistry.getRoundFee(navigator.address, 999)).to.equal(0n);
});
});
// ======================== 4. Governance Setters ======================== //
(0, mocha_1.describe)("Governance setters", function () {
(0, mocha_1.it)("should allow governance to update feeLockPeriod", async function () {
const newPeriod = 8;
await navigatorRegistry.connect(owner).setFeeLockPeriod(newPeriod);
(0, chai_1.expect)(await navigatorRegistry.getFeeLockPeriod()).to.equal(newPeriod);
});
(0, mocha_1.it)("should revert setFeeLockPeriod with zero value", async function () {
await (0, chai_1.expect)(navigatorRegistry.connect(owner).setFeeLockPeriod(0)).to.be.revertedWithCustomError(navigatorRegistry, "InvalidParameter");
});
(0, mocha_1.it)("should allow governance to update feePercentage", async function () {
const newPercentage = 3000; // 30%
await navigatorRegistry.connect(owner).setFeePercentage(newPercentage);
(0, chai_1.expect)(await navigatorRegistry.getFeePercentage()).to.equal(newPercentage);
});
(0, mocha_1.it)("should revert setFeePercentage with value exceeding basis points", async function () {
await (0, chai_1.expect)(navigatorRegistry.connect(owner).setFeePercentage(10001)).to.be.revertedWithCustomError(navigatorRegistry, "InvalidParameter");
});
});
});