UNPKG

@vechain/vebetterdao-contracts

Version:

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

391 lines (390 loc) 25.1 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const chai_1 = require("chai"); const hardhat_1 = require("hardhat"); const mocha_1 = require("mocha"); const helpers_1 = require("../../helpers"); const xnodes_1 = require("../../helpers/xnodes"); // Helper: advance to next round (wait + distribute) async function advanceToNextRound(roundId) { const { emissions, minterAccount } = contracts; await (0, helpers_1.waitForRoundToEnd)(roundId); await (0, helpers_1.waitForNextCycle)(); await emissions.connect(minterAccount).distribute(); } // Shared test state let contracts; let owner; let otherAccounts; let app1Id; let app2Id; // Helper: set production multipliers and enable passport whitelist check async function setupMultipliers() { const { voterRewards, veBetterPassport } = contracts; await voterRewards.connect(owner).grantRole(await voterRewards.GOVERNANCE_ROLE(), owner.address); await voterRewards.connect(owner).setFreshnessMultipliers(30000, 20000, 10000); await voterRewards.connect(owner).setIntentMultipliers(10000, 3000); // Enable whitelist check for personhood if (!(await veBetterPassport.isCheckEnabled(1))) { await veBetterPassport.connect(owner).toggleCheck(1); } } // Helper: create 2 endorsed apps (same pattern as VoterRewards.test.ts) async function createEndorsedApps() { const { x2EarnApps, x2EarnCreator } = contracts; // App 1: creator = otherAccounts[10] if ((await x2EarnCreator.balanceOf(otherAccounts[10].address)) === 0n) { await x2EarnCreator.connect(owner).safeMint(otherAccounts[10].address); } await x2EarnApps .connect(otherAccounts[10]) .submitApp(otherAccounts[10].address, otherAccounts[10].address, "FreshApp1", "metadataURI"); app1Id = await x2EarnApps.hashAppName("FreshApp1"); await (0, xnodes_1.endorseApp)(app1Id, otherAccounts[12]); // App 2: creator = otherAccounts[11] if ((await x2EarnCreator.balanceOf(otherAccounts[11].address)) === 0n) { await x2EarnCreator.connect(owner).safeMint(otherAccounts[11].address); } await x2EarnApps .connect(otherAccounts[11]) .submitApp(otherAccounts[11].address, otherAccounts[11].address, "FreshApp2", "metadataURI"); app2Id = await x2EarnApps.hashAppName("FreshApp2"); await (0, xnodes_1.endorseApp)(app2Id, otherAccounts[13]); } (0, mocha_1.describe)("Rewards Multipliers - @shard10b", function () { // ======================== Config Management ======================== // (0, mocha_1.describe)("Multiplier Config", function () { (0, mocha_1.it)("Should have neutral multiplier values after test deployment and allow setting production values", async () => { const { voterRewards, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); await voterRewards.connect(owner).grantRole(await voterRewards.GOVERNANCE_ROLE(), owner.address); const [ft1, ft2, ft3] = await voterRewards.getFreshnessMultipliers(0); (0, chai_1.expect)(ft1).to.equal(10000n); (0, chai_1.expect)(ft2).to.equal(10000n); (0, chai_1.expect)(ft3).to.equal(10000n); const [ifa, iab] = await voterRewards.getIntentMultipliers(0); (0, chai_1.expect)(ifa).to.equal(10000n); (0, chai_1.expect)(iab).to.equal(10000n); await voterRewards.connect(owner).setFreshnessMultipliers(30000, 20000, 10000); await voterRewards.connect(owner).setIntentMultipliers(10000, 3000); const block = await hardhat_1.ethers.provider.getBlockNumber(); const [pt1, pt2, pt3] = await voterRewards.getFreshnessMultipliers(block); (0, chai_1.expect)(pt1).to.equal(30000n); (0, chai_1.expect)(pt2).to.equal(20000n); (0, chai_1.expect)(pt3).to.equal(10000n); const [pifa, piab] = await voterRewards.getIntentMultipliers(block); (0, chai_1.expect)(pifa).to.equal(10000n); (0, chai_1.expect)(piab).to.equal(3000n); }); (0, mocha_1.it)("Should reject freshness multipliers not in descending order", async () => { const { voterRewards, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); await voterRewards.connect(owner).grantRole(await voterRewards.GOVERNANCE_ROLE(), owner.address); await (0, chai_1.expect)(voterRewards.connect(owner).setFreshnessMultipliers(10000, 20000, 10000)).to.be.reverted; }); (0, mocha_1.it)("Should reject zero multiplier values", async () => { const { voterRewards, owner } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); await voterRewards.connect(owner).grantRole(await voterRewards.GOVERNANCE_ROLE(), owner.address); await (0, chai_1.expect)(voterRewards.connect(owner).setFreshnessMultipliers(0, 0, 0)).to.be.reverted; await (0, chai_1.expect)(voterRewards.connect(owner).setIntentMultipliers(0, 0)).to.be.reverted; }); (0, mocha_1.it)("Should only allow GOVERNANCE_ROLE to set multipliers", async () => { const { voterRewards, otherAccounts } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); await (0, chai_1.expect)(voterRewards.connect(otherAccounts[5]).setFreshnessMultipliers(30000, 20000, 10000)).to.be.reverted; await (0, chai_1.expect)(voterRewards.connect(otherAccounts[5]).setIntentMultipliers(10000, 3000)).to.be.reverted; }); (0, mocha_1.it)("Should return MULTIPLIER_SCALE constant", async () => { const { voterRewards } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); (0, chai_1.expect)(await voterRewards.MULTIPLIER_SCALE()).to.equal(10000n); }); }); // ======================== Freshness Multiplier ======================== // (0, mocha_1.describe)("Freshness Multiplier - Allocation Voting", function () { (0, mocha_1.beforeEach)(async function () { contracts = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); owner = contracts.owner; otherAccounts = contracts.otherAccounts; const { veBetterPassport } = contracts; await setupMultipliers(); // Create and endorse apps BEFORE starting emissions so they're eligible in round 1 await createEndorsedApps(); // Mint tokens and whitelist voters BEFORE emissions start (so snapshot captures their balance) for (const acc of [otherAccounts[1], otherAccounts[2], otherAccounts[3]]) { await (0, helpers_1.getVot3Tokens)(acc, "100000"); await veBetterPassport.connect(owner).whitelist(acc.address); } await (0, helpers_1.bootstrapAndStartEmissions)(); }); (0, mocha_1.it)("First-time voter should get tier 1 (x3) reward weight", async () => { const { xAllocationVoting, voterRewards, emissions, veBetterPassport } = contracts; const voter = otherAccounts[1]; await (0, helpers_1.getVot3Tokens)(voter, "1000"); await veBetterPassport.connect(owner).whitelist(voter.address); const roundId = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(roundId, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle = await emissions.getCurrentCycle(); const voterTotal = await voterRewards.cycleToVoterToTotal(cycle, voter.address); (0, chai_1.expect)(voterTotal).to.be.gt(0n); }); (0, mocha_1.it)("Voter with same apps in round 2 should get tier 2 (x2) — less than round 1", async () => { const { xAllocationVoting, voterRewards, emissions, veBetterPassport } = contracts; const voter = otherAccounts[1]; await (0, helpers_1.getVot3Tokens)(voter, "10000"); await veBetterPassport.connect(owner).whitelist(voter.address); // Round 1: x3 const round1 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round1, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle1 = await emissions.getCurrentCycle(); const total1 = await voterRewards.cycleToVoterToTotal(cycle1, voter.address); await advanceToNextRound(Number(round1)); // Round 2: same apps → x2 const round2 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round2, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle2 = await emissions.getCurrentCycle(); const total2 = await voterRewards.cycleToVoterToTotal(cycle2, voter.address); (0, chai_1.expect)(total2).to.be.lt(total1); }); (0, mocha_1.it)("Voter who changes apps in round 2 should get tier 1 (x3) again", async () => { const { xAllocationVoting, voterRewards, emissions, veBetterPassport } = contracts; const voter = otherAccounts[1]; await (0, helpers_1.getVot3Tokens)(voter, "10000"); await veBetterPassport.connect(owner).whitelist(voter.address); // Round 1: app1 → x3 const round1 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round1, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle1 = await emissions.getCurrentCycle(); const total1 = await voterRewards.cycleToVoterToTotal(cycle1, voter.address); await advanceToNextRound(Number(round1)); // Round 2: app2 → fingerprint changed → x3 again const round2 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round2, [app2Id], [hardhat_1.ethers.parseEther("100")]); const cycle2 = await emissions.getCurrentCycle(); const total2 = await voterRewards.cycleToVoterToTotal(cycle2, voter.address); // Both x3 → roughly equal const ratio = (total2 * 100n) / total1; (0, chai_1.expect)(ratio).to.be.gte(90n); (0, chai_1.expect)(ratio).to.be.lte(110n); }); (0, mocha_1.it)("Voter with same apps for 3+ rounds should get tier 3 (x1) — stale", async () => { const { xAllocationVoting, voterRewards, emissions, veBetterPassport } = contracts; const voter = otherAccounts[1]; await (0, helpers_1.getVot3Tokens)(voter, "10000"); await veBetterPassport.connect(owner).whitelist(voter.address); // Round 1: x3 const round1 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round1, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle1 = await emissions.getCurrentCycle(); const total1 = await voterRewards.cycleToVoterToTotal(cycle1, voter.address); // Round 2: same → x2 await advanceToNextRound(Number(round1)); const round2 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round2, [app1Id], [hardhat_1.ethers.parseEther("100")]); // Round 3: same → x1 await advanceToNextRound(Number(round2)); const round3 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round3, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle3 = await emissions.getCurrentCycle(); const total3 = await voterRewards.cycleToVoterToTotal(cycle3, voter.address); // x1 < x3 (0, chai_1.expect)(total3).to.be.lt(total1); }); (0, mocha_1.it)("XOR fingerprint is order-independent — [A,B] and [B,A] produce same multiplier", async () => { const { xAllocationVoting, voterRewards, emissions, veBetterPassport } = contracts; const voter1 = otherAccounts[1]; const voter2 = otherAccounts[2]; await (0, helpers_1.getVot3Tokens)(voter1, "10000"); await (0, helpers_1.getVot3Tokens)(voter2, "10000"); await veBetterPassport.connect(owner).whitelist(voter1.address); await veBetterPassport.connect(owner).whitelist(voter2.address); const roundId = await xAllocationVoting.currentRoundId(); // Voter1: [app1, app2] await xAllocationVoting .connect(voter1) .castVote(roundId, [app1Id, app2Id], [hardhat_1.ethers.parseEther("50"), hardhat_1.ethers.parseEther("50")]); // Voter2: [app2, app1] — reversed await xAllocationVoting .connect(voter2) .castVote(roundId, [app2Id, app1Id], [hardhat_1.ethers.parseEther("50"), hardhat_1.ethers.parseEther("50")]); const cycle = await emissions.getCurrentCycle(); const total1 = await voterRewards.cycleToVoterToTotal(cycle, voter1.address); const total2 = await voterRewards.cycleToVoterToTotal(cycle, voter2.address); // Same fingerprint → same multiplier → same reward (0, chai_1.expect)(total1).to.equal(total2); }); (0, mocha_1.it)("Should emit FreshnessMultiplierApplied event with correct args", async () => { const { xAllocationVoting, veBetterPassport } = contracts; const voter = otherAccounts[1]; await (0, helpers_1.getVot3Tokens)(voter, "1000"); await veBetterPassport.connect(owner).whitelist(voter.address); const roundId = await xAllocationVoting.currentRoundId(); const tx = await xAllocationVoting.connect(voter).castVote(roundId, [app1Id], [hardhat_1.ethers.parseEther("100")]); const receipt = await tx.wait(); const event = receipt?.logs.find((log) => { try { const parsed = xAllocationVoting.interface.parseLog({ topics: log.topics, data: log.data }); return parsed?.name === "FreshnessMultiplierApplied"; } catch { return false; } }); (0, chai_1.expect)(event).to.not.be.undefined; const parsed = xAllocationVoting.interface.parseLog({ topics: event.topics, data: event.data }); (0, chai_1.expect)(parsed?.args.voter).to.equal(voter.address); (0, chai_1.expect)(parsed?.args.roundId).to.equal(roundId); (0, chai_1.expect)(parsed?.args.multiplier).to.equal(30000n); // x3 first vote }); (0, mocha_1.it)("hasUserVotedForApp should return correct values", async () => { const { xAllocationVoting, veBetterPassport } = contracts; const voter = otherAccounts[1]; await (0, helpers_1.getVot3Tokens)(voter, "1000"); await veBetterPassport.connect(owner).whitelist(voter.address); const roundId = await xAllocationVoting.currentRoundId(); // Before voting (0, chai_1.expect)(await xAllocationVoting.hasUserVotedForApp(roundId, voter.address, app1Id)).to.be.false; (0, chai_1.expect)(await xAllocationVoting.hasUserVotedForApp(roundId, voter.address, app2Id)).to.be.false; // Vote for app1 only await xAllocationVoting.connect(voter).castVote(roundId, [app1Id], [hardhat_1.ethers.parseEther("100")]); // After voting (0, chai_1.expect)(await xAllocationVoting.hasUserVotedForApp(roundId, voter.address, app1Id)).to.be.true; (0, chai_1.expect)(await xAllocationVoting.hasUserVotedForApp(roundId, voter.address, app2Id)).to.be.false; }); (0, mocha_1.it)("Voter skips round 2, votes same apps in round 3 — should get stale multiplier", async () => { const { xAllocationVoting, voterRewards, emissions, veBetterPassport } = contracts; const voter = otherAccounts[1]; await (0, helpers_1.getVot3Tokens)(voter, "10000"); await veBetterPassport.connect(owner).whitelist(voter.address); // Round 1: x3 const round1 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round1, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle1 = await emissions.getCurrentCycle(); const total1 = await voterRewards.cycleToVoterToTotal(cycle1, voter.address); // Skip round 2 await advanceToNextRound(Number(round1)); const round2 = await xAllocationVoting.currentRoundId(); await advanceToNextRound(Number(round2)); // Round 3: same apps → roundsSinceChange = 2 → x1 const round3 = await xAllocationVoting.currentRoundId(); await xAllocationVoting.connect(voter).castVote(round3, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle3 = await emissions.getCurrentCycle(); const total3 = await voterRewards.cycleToVoterToTotal(cycle3, voter.address); (0, chai_1.expect)(total3).to.be.lt(total1); }); }); // ======================== Governance Intent Multiplier ======================== // (0, mocha_1.describe)("Governance Intent Multiplier - Proposal Voting", function () { (0, mocha_1.beforeEach)(async function () { contracts = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); owner = contracts.owner; otherAccounts = contracts.otherAccounts; const { veBetterPassport } = contracts; await setupMultipliers(); // Create and endorse apps BEFORE starting emissions await createEndorsedApps(); // Mint tokens and whitelist before emissions (proposer needs a lot for deposit threshold) for (const acc of [otherAccounts[0], otherAccounts[1], otherAccounts[2]]) { await (0, helpers_1.getVot3Tokens)(acc, "1000000"); await veBetterPassport.connect(owner).whitelist(acc.address); } await (0, helpers_1.bootstrapAndStartEmissions)(); }); // Helper: create proposal, deposit, and wait for active async function createAndActivateProposal(proposer) { const { governor, b3tr, vot3 } = contracts; const b3trContract = await hardhat_1.ethers.getContractFactory("B3TR"); const tx = await (0, helpers_1.createProposal)(b3tr, b3trContract, proposer, `intent test ${Date.now()}`); const proposalId = await (0, helpers_1.getProposalIdFromTx)(tx); // Deposit to meet threshold const depositThreshold = await governor.proposalDepositThreshold(proposalId); const currentDeposit = await governor.getProposalDeposits(proposalId); if (currentDeposit < depositThreshold) { const remaining = depositThreshold - currentDeposit; await vot3.connect(proposer).approve(await governor.getAddress(), remaining); await governor.connect(proposer).deposit(remaining, proposalId); } await (0, helpers_1.waitForProposalToBeActive)(proposalId); return proposalId; } (0, mocha_1.it)("For vote should get full (x1) reward weight", async () => { const { governor, voterRewards, emissions } = contracts; const proposer = otherAccounts[0]; const voter = otherAccounts[1]; const proposalId = await createAndActivateProposal(proposer); await governor.connect(voter).castVote(proposalId, 1); // For const cycle = await emissions.getCurrentCycle(); const voterTotal = await voterRewards.cycleToVoterToTotal(cycle, voter.address); (0, chai_1.expect)(voterTotal).to.be.gt(0n); }); (0, mocha_1.it)("Against vote should get same reward as For", async () => { const { governor, voterRewards, emissions } = contracts; const proposer = otherAccounts[0]; const voterFor = otherAccounts[1]; const voterAgainst = otherAccounts[2]; const proposalId = await createAndActivateProposal(proposer); await governor.connect(voterFor).castVote(proposalId, 1); // For await governor.connect(voterAgainst).castVote(proposalId, 0); // Against const cycle = await emissions.getCurrentCycle(); const totalFor = await voterRewards.cycleToVoterToTotal(cycle, voterFor.address); const totalAgainst = await voterRewards.cycleToVoterToTotal(cycle, voterAgainst.address); (0, chai_1.expect)(totalFor).to.equal(totalAgainst); }); (0, mocha_1.it)("Abstain vote should get reduced reward weight compared to For", async () => { const { governor, voterRewards, emissions } = contracts; const proposer = otherAccounts[0]; const voterFor = otherAccounts[1]; const voterAbstain = otherAccounts[2]; const proposalId = await createAndActivateProposal(proposer); await governor.connect(voterFor).castVote(proposalId, 1); // For → x1 await governor.connect(voterAbstain).castVote(proposalId, 2); // Abstain → x0.30 const cycle = await emissions.getCurrentCycle(); const totalFor = await voterRewards.cycleToVoterToTotal(cycle, voterFor.address); const totalAbstain = await voterRewards.cycleToVoterToTotal(cycle, voterAbstain.address); (0, chai_1.expect)(totalAbstain).to.be.lt(totalFor); }); }); // ======================== Checkpoint Behavior ======================== // (0, mocha_1.describe)("Checkpoint Behavior", function () { (0, mocha_1.it)("Multiplier change mid-round should not affect current round votes", async () => { contracts = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); owner = contracts.owner; otherAccounts = contracts.otherAccounts; const { xAllocationVoting, voterRewards, emissions, veBetterPassport } = contracts; await voterRewards.connect(owner).grantRole(await voterRewards.GOVERNANCE_ROLE(), owner.address); // Set initial multiplier before rounds start await voterRewards.connect(owner).setFreshnessMultipliers(20000, 15000, 10000); await createEndorsedApps(); // Enable whitelist check if (!(await veBetterPassport.isCheckEnabled(1))) { await veBetterPassport.connect(owner).toggleCheck(1); } const voter1 = otherAccounts[1]; const voter2 = otherAccounts[2]; await (0, helpers_1.getVot3Tokens)(voter1, "100000"); await (0, helpers_1.getVot3Tokens)(voter2, "100000"); await veBetterPassport.connect(owner).whitelist(voter1.address); await veBetterPassport.connect(owner).whitelist(voter2.address); await (0, helpers_1.bootstrapAndStartEmissions)(); const roundId = await xAllocationVoting.currentRoundId(); // Voter1 votes before change await xAllocationVoting.connect(voter1).castVote(roundId, [app1Id], [hardhat_1.ethers.parseEther("100")]); // Change multiplier mid-round await voterRewards.connect(owner).setFreshnessMultipliers(50000, 30000, 10000); // Voter2 votes after change await xAllocationVoting.connect(voter2).castVote(roundId, [app1Id], [hardhat_1.ethers.parseEther("100")]); const cycle = await emissions.getCurrentCycle(); const total1 = await voterRewards.cycleToVoterToTotal(cycle, voter1.address); const total2 = await voterRewards.cycleToVoterToTotal(cycle, voter2.address); // Both should use the value at round snapshot → same reward (0, chai_1.expect)(total1).to.equal(total2); }); }); // ======================== Upgrade Safety ======================== // (0, mocha_1.describe)("Upgrade Safety", function () { (0, mocha_1.it)("VoterRewards V7 should preserve version and functions after upgrade chain", async () => { const { voterRewards } = await (0, helpers_1.getOrDeployContractInstances)({ forceDeploy: true }); (0, chai_1.expect)(await voterRewards.version()).to.equal("7"); const [t1, t2, t3] = await voterRewards.getFreshnessMultipliers(0); (0, chai_1.expect)(t1).to.be.gte(10000n); const [fa, ab] = await voterRewards.getIntentMultipliers(0); (0, chai_1.expect)(fa).to.be.gte(10000n); }); }); });