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