UNPKG

@vechain/vebetterdao-contracts

Version:

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

275 lines (274 loc) 19.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const mocha_1 = require("mocha"); const chai_1 = require("chai"); const hardhat_1 = require("hardhat"); const fixture_test_1 = require("./fixture.test"); const common_1 = require("../helpers/common"); /** * V11 Community Execution Framework end-to-end coverage. * * Final V11 surface: * - propose(...args, maxBudget) — single 7-arg signature * - markAsInDevelopment(id, payee, description, implementationDiscussion, contributors[]) * - updateCommunityExecution(...) — proposer or admin can edit until claimPayout() * - claimPayout(id) — anyone; pays full budget to the single registered payee * * One payout address per proposal; that wallet is then responsible for forwarding funds to * any contributors / dev team off-chain. Contributors are stored as a string[] of handles. */ (0, mocha_1.describe)("Governance - Community Execution Framework V11 - @shard4z", function () { let governor; let vot3; let b3tr; let treasury; let owner; let proposer; let otherAccounts; let veBetterPassport; let emissions; let xAllocationVoting; let minterAccount; // unused but destructured in the fixture let _voter; let _b3trContract; const E = (n) => hardhat_1.ethers.parseEther(n.toString()); (0, mocha_1.beforeEach)(async function () { const fixture = await (0, fixture_test_1.setupGovernanceFixtureWithEmissions)(); governor = fixture.governor; vot3 = fixture.vot3; b3tr = fixture.b3tr; treasury = fixture.treasury; owner = fixture.owner; proposer = fixture.proposer; _voter = fixture.voter; otherAccounts = fixture.otherAccounts; veBetterPassport = fixture.veBetterPassport; emissions = fixture.emissions; xAllocationVoting = fixture.xAllocationVoting; minterAccount = fixture.minterAccount; _b3trContract = fixture.b3trContract; await (0, fixture_test_1.setupProposer)(proposer, b3tr, vot3, minterAccount); }); /// Helper: text-only Standard proposal voted through to `Succeeded`. Optional budget. async function createAndPassTextProposal(opts = {}) { const targets = []; const values = []; const calldatas = []; const description = opts.description ?? `desc-${Math.random().toString(36).slice(2, 10)}`; const startRoundId = (await (0, common_1.getRoundId)()) + 1; const tx = await governor .connect(proposer) .propose(targets, values, calldatas, description, startRoundId, 0, opts.budget ?? 0); const proposalId = await (0, common_1.getProposalIdFromTx)(tx); const proposalDepositThreshold = await governor.proposalDepositThreshold(proposalId); await (0, fixture_test_1.setupSupporter)(proposer, vot3, proposalDepositThreshold, governor); await governor.connect(proposer).deposit(proposalDepositThreshold, proposalId); await (0, common_1.waitForCurrentRoundToEnd)(); await (0, fixture_test_1.setupVoter)(otherAccounts[0], b3tr, vot3, minterAccount, owner, veBetterPassport); await (0, fixture_test_1.setupVoter)(otherAccounts[1], b3tr, vot3, minterAccount, owner, veBetterPassport); await (0, fixture_test_1.setupVoter)(otherAccounts[2], b3tr, vot3, minterAccount, owner, veBetterPassport); await (0, common_1.startNewAllocationRound)({ emissions, xAllocationVoting }); await (0, common_1.waitForProposalToBeActive)(proposalId, { governor }); await governor.connect(otherAccounts[0]).castVote(proposalId, 1); await governor.connect(otherAccounts[1]).castVote(proposalId, 1); await governor.connect(otherAccounts[2]).castVote(proposalId, 1); await (0, common_1.waitForVotingPeriodToEnd)(proposalId); return proposalId; } // ------------------- propose(7) / budget storage ------------------- // (0, mocha_1.describe)("propose budget", () => { (0, mocha_1.it)("records maxBudget > 0 on chain and emits ProposalBudgetSet", async () => { const budget = E(1000); const targets = []; const values = []; const calldatas = []; const description = `budget-set-${Date.now()}`; const startRoundId = (await (0, common_1.getRoundId)()) + 1; await (0, chai_1.expect)(governor.connect(proposer).propose(targets, values, calldatas, description, startRoundId, 0, budget)).to.emit(governor, "ProposalBudgetSet"); const proposalId = await governor.hashProposal(targets, values, calldatas, hardhat_1.ethers.keccak256(hardhat_1.ethers.toUtf8Bytes(description))); (0, chai_1.expect)(await governor.getProposalBudget(proposalId)).to.equal(budget); }); (0, mocha_1.it)("propose with maxBudget == 0 records no budget", async () => { const targets = []; const values = []; const calldatas = []; const description = `legacy-${Date.now()}`; const startRoundId = (await (0, common_1.getRoundId)()) + 1; const tx = await governor.connect(proposer).propose(targets, values, calldatas, description, startRoundId, 0, 0); const proposalId = await (0, common_1.getProposalIdFromTx)(tx); (0, chai_1.expect)(await governor.getProposalBudget(proposalId)).to.equal(0n); }); }); // ------------------- markAsInDevelopment ------------------- // (0, mocha_1.describe)("markAsInDevelopment", () => { (0, mocha_1.it)("proposer can register a single payee and transition to InDevelopment", async () => { const budget = E(1000); const proposalId = await createAndPassTextProposal({ budget }); const payee = otherAccounts[5].address; const contributors = ["github:alice", "twitter:@bob"]; await (0, chai_1.expect)(governor .connect(proposer) .markAsInDevelopment(proposalId, payee, "Developed by Alice & Bob", "https://forum.example/123", contributors)) .to.emit(governor, "ProposalInDevelopment") .and.to.emit(governor, "ProposalInDevelopmentDetails") .and.to.emit(governor, "ProposalContributorsSet"); (0, chai_1.expect)(await governor.state(proposalId)).to.equal(8); // InDevelopment (0, chai_1.expect)(await governor.getProposalPayee(proposalId)).to.equal(payee); (0, chai_1.expect)(await governor.getProposalDescription(proposalId)).to.equal("Developed by Alice & Bob"); (0, chai_1.expect)(await governor.getProposalImplementationDiscussion(proposalId)).to.equal("https://forum.example/123"); (0, chai_1.expect)(await governor.getProposalContributors(proposalId)).to.deep.equal(contributors); (0, chai_1.expect)(await governor.isProposalPaid(proposalId)).to.equal(false); }); (0, mocha_1.it)("PROPOSAL_STATE_MANAGER_ROLE admin can register on behalf of the proposer", async () => { const proposalId = await createAndPassTextProposal({ budget: E(500) }); await (0, chai_1.expect)(governor .connect(owner) .markAsInDevelopment(proposalId, otherAccounts[5].address, "desc", "https://x.example", [])).to.not.be.reverted; (0, chai_1.expect)(await governor.state(proposalId)).to.equal(8); }); (0, mocha_1.it)("an unrelated account cannot register", async () => { const proposalId = await createAndPassTextProposal({ budget: E(100) }); await (0, chai_1.expect)(governor.connect(otherAccounts[7]).markAsInDevelopment(proposalId, otherAccounts[5].address, "n", "l", [])).to.be.revertedWithCustomError(governor, "UnauthorizedCommunityExecution"); }); (0, mocha_1.it)("reverts when budget > 0 and payee is the zero address", async () => { const proposalId = await createAndPassTextProposal({ budget: E(100) }); await (0, chai_1.expect)(governor.connect(proposer).markAsInDevelopment(proposalId, hardhat_1.ethers.ZeroAddress, "n", "l", [])).to.be.revertedWithCustomError(governor, "InvalidPayeeAddress"); }); (0, mocha_1.it)("reverts when budget == 0 but a payee is provided", async () => { const proposalId = await createAndPassTextProposal({}); await (0, chai_1.expect)(governor.connect(proposer).markAsInDevelopment(proposalId, otherAccounts[5].address, "n", "l", [])).to.be.revertedWithCustomError(governor, "MissingProposalBudget"); }); (0, mocha_1.it)("allows budget == 0 with zero payee (pure state transition, no payout flow)", async () => { const proposalId = await createAndPassTextProposal({}); await (0, chai_1.expect)(governor.connect(proposer).markAsInDevelopment(proposalId, hardhat_1.ethers.ZeroAddress, "desc", "https://x", [])).to.emit(governor, "ProposalInDevelopment"); (0, chai_1.expect)(await governor.state(proposalId)).to.equal(8); (0, chai_1.expect)(await governor.getProposalPayee(proposalId)).to.equal(hardhat_1.ethers.ZeroAddress); }); (0, mocha_1.it)("calling twice reverts (state already InDevelopment)", async () => { const proposalId = await createAndPassTextProposal({ budget: E(100) }); await governor.connect(proposer).markAsInDevelopment(proposalId, otherAccounts[5].address, "n", "l", []); await (0, chai_1.expect)(governor.connect(proposer).markAsInDevelopment(proposalId, otherAccounts[5].address, "n", "l", [])).to.be.revertedWithCustomError(governor, "GovernorUnexpectedProposalState"); }); (0, mocha_1.it)("reverts when too many contributors are passed", async () => { const proposalId = await createAndPassTextProposal({ budget: E(100) }); const contributors = Array.from({ length: 21 }, (_, i) => `gh:user${i}`); await (0, chai_1.expect)(governor.connect(proposer).markAsInDevelopment(proposalId, otherAccounts[5].address, "n", "l", contributors)).to.be.revertedWithCustomError(governor, "TooManyContributors"); }); }); // ------------------- updateCommunityExecution ------------------- // (0, mocha_1.describe)("updateCommunityExecution", () => { (0, mocha_1.it)("proposer can update the payee + metadata until the payout is claimed", async () => { const proposalId = await createAndPassTextProposal({ budget: E(500) }); await governor .connect(proposer) .markAsInDevelopment(proposalId, otherAccounts[5].address, "v1 desc", "https://v1", ["gh:a"]); const newPayee = otherAccounts[6].address; await (0, chai_1.expect)(governor .connect(proposer) .updateCommunityExecution(proposalId, newPayee, "v2 desc", "https://v2", ["gh:c", "gh:d"])) .to.emit(governor, "ProposalInDevelopmentDetails") .and.to.emit(governor, "ProposalContributorsSet"); (0, chai_1.expect)(await governor.getProposalPayee(proposalId)).to.equal(newPayee); (0, chai_1.expect)(await governor.getProposalDescription(proposalId)).to.equal("v2 desc"); (0, chai_1.expect)(await governor.getProposalContributors(proposalId)).to.deep.equal(["gh:c", "gh:d"]); }); (0, mocha_1.it)("admin can update even when caller is not the proposer", async () => { const proposalId = await createAndPassTextProposal({ budget: E(500) }); await governor.connect(proposer).markAsInDevelopment(proposalId, otherAccounts[5].address, "v1", "https://v1", []); await (0, chai_1.expect)(governor .connect(owner) .updateCommunityExecution(proposalId, otherAccounts[6].address, "admin", "https://admin", [])).to.not.be.reverted; (0, chai_1.expect)(await governor.getProposalPayee(proposalId)).to.equal(otherAccounts[6].address); }); (0, mocha_1.it)("unauthorized caller is rejected", async () => { const proposalId = await createAndPassTextProposal({ budget: E(500) }); await governor.connect(proposer).markAsInDevelopment(proposalId, otherAccounts[5].address, "v1", "https://v1", []); await (0, chai_1.expect)(governor.connect(otherAccounts[7]).updateCommunityExecution(proposalId, otherAccounts[6].address, "n", "l", [])).to.be.revertedWithCustomError(governor, "UnauthorizedCommunityExecution"); }); (0, mocha_1.it)("reverts after the payout has been claimed", async () => { const budget = E(100); const proposalId = await createAndPassTextProposal({ budget }); await governor.connect(proposer).markAsInDevelopment(proposalId, otherAccounts[5].address, "v1", "https://v1", []); await governor.connect(owner).markAsCompleted(proposalId); await b3tr.connect(minterAccount).mint(await treasury.getAddress(), budget); await governor.connect(otherAccounts[8]).claimPayout(proposalId); await (0, chai_1.expect)(governor .connect(proposer) .updateCommunityExecution(proposalId, otherAccounts[6].address, "v2", "https://v2", [])).to.be.revertedWithCustomError(governor, "PayoutAlreadyClaimed"); }); }); // ------------------- claimPayout ------------------- // (0, mocha_1.describe)("claimPayout", () => { (0, mocha_1.it)("anyone can trigger the payout; the full budget goes to the registered payee; idempotent", async () => { const budget = E(900); const proposalId = await createAndPassTextProposal({ budget }); const payee = otherAccounts[5].address; await governor.connect(proposer).markAsInDevelopment(proposalId, payee, "desc", "https://x", ["gh:a", "gh:b"]); await governor.connect(owner).markAsCompleted(proposalId); await b3tr.connect(minterAccount).mint(await treasury.getAddress(), budget); const before = await b3tr.balanceOf(payee); await (0, chai_1.expect)(governor.connect(otherAccounts[8]).claimPayout(proposalId)).to.emit(governor, "ProposalPayoutClaimed"); (0, chai_1.expect)(await b3tr.balanceOf(payee)).to.equal(before + budget); (0, chai_1.expect)(await governor.isProposalPaid(proposalId)).to.equal(true); await (0, chai_1.expect)(governor.connect(otherAccounts[8]).claimPayout(proposalId)).to.be.revertedWithCustomError(governor, "PayoutAlreadyClaimed"); }); (0, mocha_1.it)("claim reverts before the proposal is Completed", async () => { const proposalId = await createAndPassTextProposal({ budget: E(100) }); await governor.connect(proposer).markAsInDevelopment(proposalId, otherAccounts[5].address, "n", "l", []); await b3tr.connect(minterAccount).mint(await treasury.getAddress(), E(100)); await (0, chai_1.expect)(governor.connect(owner).claimPayout(proposalId)).to.be.revertedWithCustomError(governor, "GovernorUnexpectedProposalState"); }); (0, mocha_1.it)("claim reverts when the proposal has no budget / no payee", async () => { const proposalId = await createAndPassTextProposal({}); await governor.connect(proposer).markAsInDevelopment(proposalId, hardhat_1.ethers.ZeroAddress, "desc", "https://x", []); await governor.connect(owner).markAsCompleted(proposalId); await (0, chai_1.expect)(governor.connect(owner).claimPayout(proposalId)).to.be.revertedWithCustomError(governor, "NotReadyToClaim"); }); }); // ------------------- resetDevelopmentState (regression) ------------------- // (0, mocha_1.describe)("resetDevelopmentState", () => { (0, mocha_1.it)("wipes V11 community-execution metadata so markAsInDevelopment can re-run with a fresh payee", async () => { const budget = E(500); const proposalId = await createAndPassTextProposal({ budget }); const payeeA = otherAccounts[5].address; const payeeB = otherAccounts[6].address; await governor .connect(proposer) .markAsInDevelopment(proposalId, payeeA, "first desc", "https://first", ["gh:alice"]); // sanity: first registration is observable (0, chai_1.expect)(await governor.getProposalPayee(proposalId)).to.equal(payeeA); (0, chai_1.expect)(await governor.getProposalDescription(proposalId)).to.equal("first desc"); (0, chai_1.expect)(await governor.getProposalContributors(proposalId)).to.deep.equal(["gh:alice"]); // Admin resets — V11 metadata must be wiped await (0, chai_1.expect)(governor.connect(owner).resetDevelopmentState(proposalId)).to.emit(governor, "ProposalDevelopmentStateReset"); (0, chai_1.expect)(await governor.getProposalPayee(proposalId)).to.equal(hardhat_1.ethers.ZeroAddress); (0, chai_1.expect)(await governor.getProposalDescription(proposalId)).to.equal(""); (0, chai_1.expect)(await governor.getProposalImplementationDiscussion(proposalId)).to.equal(""); (0, chai_1.expect)(await governor.getProposalContributors(proposalId)).to.deep.equal([]); // markAsInDevelopment can now be called again with a different payee await (0, chai_1.expect)(governor.connect(proposer).markAsInDevelopment(proposalId, payeeB, "second desc", "https://second", ["gh:bob"])).to.emit(governor, "ProposalInDevelopment"); (0, chai_1.expect)(await governor.getProposalPayee(proposalId)).to.equal(payeeB); (0, chai_1.expect)(await governor.getProposalDescription(proposalId)).to.equal("second desc"); (0, chai_1.expect)(await governor.getProposalContributors(proposalId)).to.deep.equal(["gh:bob"]); }); (0, mocha_1.it)("reverts with PayoutAlreadyClaimed after claimPayout has been called", async () => { const budget = E(700); const proposalId = await createAndPassTextProposal({ budget }); const payee = otherAccounts[5].address; await governor.connect(proposer).markAsInDevelopment(proposalId, payee, "desc", "https://x", []); await governor.connect(owner).markAsCompleted(proposalId); await b3tr.connect(minterAccount).mint(await treasury.getAddress(), budget); await governor.connect(otherAccounts[8]).claimPayout(proposalId); await (0, chai_1.expect)(governor.connect(owner).resetDevelopmentState(proposalId)) .to.be.revertedWithCustomError(governor, "PayoutAlreadyClaimed") .withArgs(proposalId); }); }); // ------------------- misc ------------------- // (0, mocha_1.describe)("misc", () => { (0, mocha_1.it)("version() reports 11", async () => { (0, chai_1.expect)(await governor.version()).to.equal("11"); }); }); });