UNPKG

@vechain/vebetterdao-contracts

Version:

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

248 lines (247 loc) 15.1 kB
import { ethers } from "hardhat"; import { expect } from "chai"; import { describe, it, beforeEach } from "mocha"; import { createLocalConfig } from "@repo/config/contracts/envs/local"; import { getOrDeployContractInstances } from "../helpers/deploy"; import { bootstrapAndStartEmissions, getVot3Tokens, waitForRoundToEnd, } from "../helpers/common"; describe("NavigatorRegistry Fees - @shard19d", function () { let navigatorRegistry; let b3tr; let voterRewards; let emissions; let xAllocationVoting; let owner; let otherAccounts; let minterAccount; const config = createLocalConfig(); const STAKE_AMOUNT = ethers.parseEther("50000"); const METADATA_URI = "ipfs://navigator-metadata"; const FEE_AMOUNT = 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 ethers.provider.send("hardhat_impersonateAccount", [voterRewardsAddress]); await ethers.provider.send("hardhat_setBalance", [voterRewardsAddress, "0x" + (10n ** 18n).toString(16)]); const voterRewardsSigner = await 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 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 waitForRoundToEnd(Number(roundId)); await emissions.distribute(); } }; beforeEach(async function () { const deployment = await 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, ethers.parseEther("10000000")); // Create VOT3 supply (max stake = 1% of VOT3 supply, need >= 5M VOT3 for 50k stake) await getVot3Tokens(otherAccounts[15], "10000000"); }); // ======================== 1. depositNavigatorFee ======================== // describe("depositNavigatorFee", function () { it("should deposit fee and record it in getRoundFee", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await bootstrapAndStartEmissions(); const roundId = await xAllocationVoting.currentRoundId(); await depositFeeViaImpersonation(navigator.address, roundId, FEE_AMOUNT); expect(await navigatorRegistry.getRoundFee(navigator.address, roundId)).to.equal(FEE_AMOUNT); }); it("should revert when called by non-VoterRewards address", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await bootstrapAndStartEmissions(); const roundId = await xAllocationVoting.currentRoundId(); await expect(navigatorRegistry.connect(owner).depositNavigatorFee(navigator.address, roundId, FEE_AMOUNT)).to.be.revertedWithCustomError(navigatorRegistry, "UnauthorizedCaller"); }); it("should accumulate multiple deposits in the same round", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await bootstrapAndStartEmissions(); const roundId = await xAllocationVoting.currentRoundId(); const firstDeposit = ethers.parseEther("50"); const secondDeposit = ethers.parseEther("75"); await depositFeeViaImpersonation(navigator.address, roundId, firstDeposit); await depositFeeViaImpersonation(navigator.address, roundId, secondDeposit); expect(await navigatorRegistry.getRoundFee(navigator.address, roundId)).to.equal(firstDeposit + secondDeposit); }); it("should emit FeeDeposited event", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await bootstrapAndStartEmissions(); const roundId = await xAllocationVoting.currentRoundId(); const voterRewardsAddress = await voterRewards.getAddress(); await ethers.provider.send("hardhat_impersonateAccount", [voterRewardsAddress]); await ethers.provider.send("hardhat_setBalance", [voterRewardsAddress, "0x" + (10n ** 18n).toString(16)]); const voterRewardsSigner = await ethers.getSigner(voterRewardsAddress); const registryAddress = await navigatorRegistry.getAddress(); await b3tr.connect(owner).transfer(registryAddress, FEE_AMOUNT); await expect(navigatorRegistry.connect(voterRewardsSigner).depositNavigatorFee(navigator.address, roundId, FEE_AMOUNT)) .to.emit(navigatorRegistry, "FeeDeposited") .withArgs(navigator.address, roundId, FEE_AMOUNT); await ethers.provider.send("hardhat_stopImpersonatingAccount", [voterRewardsAddress]); }); }); // ======================== 2. claimFee ======================== // describe("claimFee", function () { it("should allow claiming after feeLockPeriod rounds", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await 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); expect(balanceAfter - balanceBefore).to.equal(FEE_AMOUNT); }); it("should revert when fees are still locked", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await 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 expect(navigatorRegistry.connect(navigator).claimFee(depositRoundId)).to.be.revertedWithCustomError(navigatorRegistry, "FeesStillLocked"); }); it("should revert when no fees exist for the round", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await bootstrapAndStartEmissions(); const roundId = await xAllocationVoting.currentRoundId(); // Advance past lock period with no deposited fees const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod()); await advanceRounds(lockPeriod); await expect(navigatorRegistry.connect(navigator).claimFee(roundId)).to.be.revertedWithCustomError(navigatorRegistry, "NoFeesToClaim"); }); it("should revert when fees have been forfeited via deactivateNavigator with slashFees=true", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await 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 expect(navigatorRegistry.connect(navigator).claimFee(depositRoundId)).to.be.revertedWithCustomError(navigatorRegistry, "NotRegistered"); }); it("should revert on double claim", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await 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 expect(navigatorRegistry.connect(navigator).claimFee(depositRoundId)).to.be.revertedWithCustomError(navigatorRegistry, "NoFeesToClaim"); }); it("should emit FeeClaimed event", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await bootstrapAndStartEmissions(); const depositRoundId = await xAllocationVoting.currentRoundId(); await depositFeeViaImpersonation(navigator.address, depositRoundId, FEE_AMOUNT); const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod()); await advanceRounds(lockPeriod); await expect(navigatorRegistry.connect(navigator).claimFee(depositRoundId)) .to.emit(navigatorRegistry, "FeeClaimed") .withArgs(navigator.address, depositRoundId, FEE_AMOUNT); }); it("should clear round fee after successful claim", async function () { const navigator = otherAccounts[10]; await registerNavigator(navigator); await 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); expect(await navigatorRegistry.getRoundFee(navigator.address, depositRoundId)).to.equal(0n); }); }); // ======================== 3. View Functions ======================== // describe("View functions", function () { it("getFeeLockPeriod returns configured value", async function () { expect(await navigatorRegistry.getFeeLockPeriod()).to.equal(config.NAVIGATOR_FEE_LOCK_PERIOD); }); it("getFeePercentage returns configured value", async function () { expect(await navigatorRegistry.getFeePercentage()).to.equal(config.NAVIGATOR_FEE_PERCENTAGE); }); it("isRoundFeeUnlocked returns false when still locked", async function () { await bootstrapAndStartEmissions(); const depositRoundId = await xAllocationVoting.currentRoundId(); // Advance 2 rounds (less than lock period of 4) await advanceRounds(2); expect(await navigatorRegistry.isRoundFeeUnlocked(depositRoundId)).to.equal(false); }); it("isRoundFeeUnlocked returns true after lock period", async function () { await bootstrapAndStartEmissions(); const depositRoundId = await xAllocationVoting.currentRoundId(); const lockPeriod = Number(await navigatorRegistry.getFeeLockPeriod()); await advanceRounds(lockPeriod); expect(await navigatorRegistry.isRoundFeeUnlocked(depositRoundId)).to.equal(true); }); it("getRoundFee returns 0 for rounds with no deposits", async function () { const navigator = otherAccounts[10]; expect(await navigatorRegistry.getRoundFee(navigator.address, 999)).to.equal(0n); }); }); // ======================== 4. Governance Setters ======================== // describe("Governance setters", function () { it("should allow governance to update feeLockPeriod", async function () { const newPeriod = 8; await navigatorRegistry.connect(owner).setFeeLockPeriod(newPeriod); expect(await navigatorRegistry.getFeeLockPeriod()).to.equal(newPeriod); }); it("should revert setFeeLockPeriod with zero value", async function () { await expect(navigatorRegistry.connect(owner).setFeeLockPeriod(0)).to.be.revertedWithCustomError(navigatorRegistry, "InvalidParameter"); }); it("should allow governance to update feePercentage", async function () { const newPercentage = 3000; // 30% await navigatorRegistry.connect(owner).setFeePercentage(newPercentage); expect(await navigatorRegistry.getFeePercentage()).to.equal(newPercentage); }); it("should revert setFeePercentage with value exceeding basis points", async function () { await expect(navigatorRegistry.connect(owner).setFeePercentage(10001)).to.be.revertedWithCustomError(navigatorRegistry, "InvalidParameter"); }); }); });