@vechain/vebetterdao-contracts
Version:
Open-source repository that houses the smart contracts powering the decentralized VeBetterDAO on the VeChain Thor blockchain.
676 lines • 74.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const mocha_1 = require("mocha");
const fixture_test_1 = require("../fixture.test");
const typechain_types_1 = require("../../../typechain-types");
const hardhat_1 = require("hardhat");
const chai_1 = require("chai");
const common_1 = require("../../helpers/common");
(0, mocha_1.describe)("Governance - Milestone Creation - @shard4c", function () {
let governor;
let vot3;
let b3tr;
let minterAccount;
let proposer;
let secondaryAccount;
let treasury;
let grantsManager;
let owner;
let voter;
let veBetterPassport;
let timeLock;
let grantsManagerAddress;
let treasuryAddress;
let emissions;
let xAllocationVoting;
let contractToPassToMethods;
let treasuryContract;
let grantsManagerInterface;
let governorProposalLogicInterface;
let governorInterface;
(0, mocha_1.beforeEach)(async function () {
const fixture = await (0, fixture_test_1.setupGovernanceFixtureWithEmissions)();
governor = fixture.governor;
vot3 = fixture.vot3;
b3tr = fixture.b3tr;
minterAccount = fixture.minterAccount;
proposer = fixture.proposer;
secondaryAccount = fixture.otherAccount;
treasury = fixture.treasury;
grantsManager = fixture.grantsManager;
owner = fixture.owner;
voter = fixture.voter;
veBetterPassport = fixture.veBetterPassport;
timeLock = fixture.timeLock;
emissions = fixture.emissions;
xAllocationVoting = fixture.xAllocationVoting;
// Setup proposer for all tests
await emissions.connect(minterAccount).start();
await (0, fixture_test_1.setupProposer)(proposer, b3tr, vot3, minterAccount);
await vot3.connect(proposer).approve(await governor.getAddress(), hardhat_1.ethers.parseEther("1000"));
grantsManagerAddress = await grantsManager.getAddress();
treasuryAddress = await treasury.getAddress();
treasuryContract = await hardhat_1.ethers.getContractFactory("Treasury");
contractToPassToMethods = {
b3tr,
vot3,
minterAccount,
governor,
treasury,
emissions,
xAllocationVoting,
veBetterPassport,
owner,
timeLock,
grantsManager,
};
grantsManagerInterface = typechain_types_1.GrantsManager__factory.createInterface();
governorProposalLogicInterface = typechain_types_1.GovernorProposalLogic__factory.createInterface();
governorInterface = typechain_types_1.B3TRGovernor__factory.createInterface();
});
(0, mocha_1.describe)("Milestone contract setup and creation", function () {
(0, mocha_1.it)("Should set the minimum milestone count", async function () {
const minimumMilestoneCount = await grantsManager.getMinimumMilestoneCount();
(0, chai_1.expect)(minimumMilestoneCount).to.equal(2); // MINIMUM_MILESTONE_COUNT = 2
});
(0, mocha_1.it)("Should have the correct governor role setup", async function () {
const governorProxyAddress = await governor.getAddress();
// Check if the governor proxy has the GOVERNANCE_ROLE
const GOVERNANCE_ROLE = await grantsManager.GOVERNANCE_ROLE();
const hasRole = await grantsManager.hasRole(GOVERNANCE_ROLE, governorProxyAddress);
((0, chai_1.expect)(hasRole).to.be.true, "Governor proxy should have GOVERNANCE_ROLE");
// Verify that the governor is correctly set in the GrantsManager
const storedGovernor = await grantsManager.getGovernorContract();
(0, chai_1.expect)(storedGovernor).to.equal(governorProxyAddress, "GrantsManager should have the correct governor address");
});
(0, mocha_1.it)("Should not create the proposal if the number of milestones is less than the minimum milestone accepted", async function () {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000")];
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const calldatas = [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]])];
(0, chai_1.expect)(governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury targets for transfers
[0], // transferb3tr is not payable
calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "InvalidNumberOfMilestones");
});
(0, mocha_1.it)("Should not create the proposal if the number of milestones is not equal to the number of values", async function () {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("20000")];
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const calldatas = [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]])];
(0, chai_1.expect)(governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury targets for transfers
[0], // transferb3tr is not payable
calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "InvalidNumberOfMilestones");
});
(0, mocha_1.it)("Should emit MilestonesCreated event when proposal is created", async function () {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("20000")];
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const calldatas = [
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]]),
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[1]]),
];
(0, chai_1.expect)(governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
[0n, 0n], // transferb3tr is not payable
calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.emit(grantsManager, "MilestonesCreated");
});
(0, mocha_1.it)("Should create milestones on proposal creation", async function () {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("20000")];
const totalAmount = values.reduce((a, b) => a + b, 0n);
const calldatas = [
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]]),
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[1]]),
];
const tx = await (0, common_1.createGrantProposal)(proposer, [treasuryAddress, treasuryAddress], // n time = lenght of the calldatas
calldatas, [0n, 0n], description, 0, proposer.address, milestonesDetailsMetadataURI, contractToPassToMethods);
const receipt = await tx.wait();
const { proposalId } = await (0, fixture_test_1.validateProposalEvents)(governor, receipt, Number(fixture_test_1.GRANT_PROPOSAL_TYPE), proposer.address, description);
// Verify milestone data was created during proposal creation
const storedGrant = await grantsManager.getGrantProposal(proposalId);
(0, chai_1.expect)(storedGrant.totalAmount).to.equal(totalAmount);
(0, chai_1.expect)(storedGrant.proposer).to.equal(proposer.address);
(0, chai_1.expect)(storedGrant.milestones.length).to.equal(calldatas.length);
(0, chai_1.expect)(storedGrant.milestones[0].amount).to.equal(values[0]);
(0, chai_1.expect)(storedGrant.milestones[1].amount).to.equal(values[1]);
(0, chai_1.expect)(storedGrant.milestones[0].isClaimed).to.equal(false);
(0, chai_1.expect)(storedGrant.milestones[1].isClaimed).to.equal(false);
(0, chai_1.expect)(storedGrant.milestones[0].isApproved).to.equal(false);
(0, chai_1.expect)(storedGrant.milestones[1].isApproved).to.equal(false);
(0, chai_1.expect)(storedGrant.milestones[0].isRejected).to.equal(false);
(0, chai_1.expect)(storedGrant.milestones[1].isRejected).to.equal(false);
(0, chai_1.expect)(storedGrant.metadataURI).to.equal(milestonesDetailsMetadataURI);
// get the state of the milestone
const firstMilestoneState = await grantsManager.milestoneState(proposalId, 0);
const secondMilestoneState = await grantsManager.milestoneState(proposalId, 1);
(0, chai_1.expect)(firstMilestoneState).to.equal(0); // Pending
(0, chai_1.expect)(secondMilestoneState).to.equal(0); // Pending
// get the grant state
const grantState = await grantsManager.grantState(proposalId);
(0, chai_1.expect)(grantState).to.equal(0); // Pending state <=> the proposal state
});
(0, mocha_1.it)("Cannot create the milestone if the function executable is not transferB3TR", async () => {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("20000")];
// get the roundId
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const calldatas = [treasury.interface.encodeFunctionData("transferVET", [grantsManagerAddress, values[0]])];
(0, chai_1.expect)(governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
[0], // Transfer total amount
calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "InvalidFunctionSelector");
});
(0, mocha_1.it)("Cannot create milestone if the value passed is 0", async () => {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("0")];
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const calldatas = [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]])];
(0, chai_1.expect)(governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
[0], // transferb3tr is not payable
calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "InvalidAmount");
});
(0, mocha_1.it)("Should not create grant proposal with invalid data", async () => {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("20000")];
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
// 2 calldatas, but only 1 target -> should revert
const calldatas = [
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]]),
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[1]]),
];
(0, chai_1.expect)(governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
[0], // 2 calldatas, but only 1 value -> should revert
calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.be.revertedWithCustomError({
interface: governorProposalLogicInterface,
}, "GovernorInvalidProposalLength");
});
(0, mocha_1.it)("Should create proposal respecting the minVotingDelay", async () => {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("20000")];
await governor.setMinVotingDelay(3); // normally set to 1
await (0, common_1.moveBlocks)(17);
let currentBlock = await governor.clock();
let currentRoundsEndsAt = await xAllocationVoting.currentRoundDeadline();
let minVotingDelay = await governor.minVotingDelay();
(0, chai_1.expect)(minVotingDelay).to.be.greaterThan(currentRoundsEndsAt - currentBlock);
const calldatas = [
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]]),
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[1]]),
];
let voteStartsInRoundId = (await xAllocationVoting.currentRoundId()) + 1n; // starts in next round
(0, chai_1.expect)(governor.connect(proposer).proposeGrant([treasuryAddress, treasuryAddress], // Only Treasury for now
[0, 0], // transferb3tr is not payable
calldatas, description, voteStartsInRoundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.be.revertedWithCustomError({
interface: governorProposalLogicInterface,
}, "GovernorInvalidStartRound");
// simulate start of new round with enough voting delay
await (0, common_1.waitForCurrentRoundToEnd)();
await emissions.distribute();
//not moving blocks, so the proposal should be in the next round
currentBlock = await governor.clock();
currentRoundsEndsAt = await xAllocationVoting.currentRoundDeadline();
minVotingDelay = await governor.minVotingDelay();
(0, chai_1.expect)(minVotingDelay).to.not.be.greaterThan(currentRoundsEndsAt - currentBlock);
// Now if we create a proposal it should not revert ( within the voting window )
voteStartsInRoundId = (await xAllocationVoting.currentRoundId()) + 1n; // starts in next round
await governor.connect(proposer).proposeGrant([treasuryAddress, treasuryAddress], // Only Treasury for now
[0, 0], // transferb3tr is not payable
calldatas, description, voteStartsInRoundId, 0, proposer.address, milestonesDetailsMetadataURI);
});
(0, mocha_1.it)("Should not be able to call createMilestone if it's not the governor", async () => {
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
await (0, chai_1.expect)(grantsManager
.connect(owner)
.createMilestones(milestonesDetailsMetadataURI, 0, proposer.address, owner.address, [], {})).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "NotAuthorized");
});
});
(0, mocha_1.describe)("Milestone execution", function () {
(0, mocha_1.it)("Should send the total amount of the proposal from the treasury to the grants manager", async () => {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("20000")];
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr
"0", // deposit amount
proposer.address, milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
const totalAmountFromProposal = await grantsManager.getTotalAmountForMilestones(proposalId);
(0, chai_1.expect)(totalAmountFromProposal).to.equal(values.reduce((a, b) => a + b, 0n));
(0, chai_1.expect)(await b3tr.balanceOf(grantsManagerAddress)).to.equal(totalAmountFromProposal);
});
(0, mocha_1.it)("Should create a milestone in pending state", async () => {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const calldatas = [
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")]),
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")]),
];
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const tx = await governor.connect(proposer).proposeGrant([treasuryAddress, treasuryAddress], // Only Treasury for now
[0, 0], // transferb3tr is not payable
calldatas, description, roundId, 0, // depositAmount
proposer.address, milestonesDetailsMetadataURI);
const receipt = await tx.wait();
const { proposalId } = await (0, fixture_test_1.validateProposalEvents)(governor, receipt, Number(fixture_test_1.GRANT_PROPOSAL_TYPE), proposer.address, description);
const milestoneStatus = await grantsManager.milestoneState(proposalId, 0);
(0, chai_1.expect)(milestoneStatus).to.equal(0);
});
(0, mocha_1.it)("Once the proposal is executed, the milestone can be validated by the GRANTS_APPROVER_ROLE or the governor", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, hardhat_1.ethers.parseEther("1")],
[grantsManagerAddress, hardhat_1.ethers.parseEther("1")],
], // args of transferb3tr
"0", // deposit amount
proposer.address, milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
// try to approve without having the GRANTS_APPROVER_ROLE (owner already have it in deploy.ts)
await (0, chai_1.expect)(grantsManager.connect(proposer).approveMilestones(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "NotAuthorized");
// grant owner the approver role
await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
await grantsManager.connect(owner).approveMilestones(proposalId, 0);
const milestonseRegistered = await grantsManager.getMilestone(proposalId, 0); // the first one should be validated
(0, chai_1.expect)(milestonseRegistered.isApproved).to.equal(true); // Approved
(0, chai_1.expect)(milestonseRegistered.amount).to.equal(hardhat_1.ethers.parseEther("1"));
// other milestone should be pending
const milestonseRegistered2 = await grantsManager.getMilestone(proposalId, 1); // the second one should be pending
(0, chai_1.expect)(milestonseRegistered2.isApproved).to.equal(false); // Pending
});
(0, mocha_1.it)("Should not be able to validate the milestone if the proposal is not executed or did not pass the deposit or voting threshold", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
// set to 1 milestone for simplicity
await grantsManager.setMinimumMilestoneCount(1);
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const tx = await governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
[0], // transferb3tr is not payable
[treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")])], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
const receipt = await tx.wait();
const { proposalId } = await (0, fixture_test_1.validateProposalEvents)(governor, receipt, Number(fixture_test_1.GRANT_PROPOSAL_TYPE), proposer.address, description);
const milestone = await grantsManager.getMilestone(proposalId, 0);
(0, chai_1.expect)(milestone.isApproved).to.equal(false); // Pending
(0, chai_1.expect)(milestone.isClaimed).to.equal(false); // Pending
// grant owner the approver role
await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
await (0, chai_1.expect)(grantsManager.connect(owner).approveMilestones(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "ProposalNotExecuted");
});
(0, mocha_1.it)("Only admin can approve the milestones", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
// set to 1 milestone for simplicity
await grantsManager.setMinimumMilestoneCount(1);
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const tx = await governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
[0], // transferb3tr is not payable
[treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")])], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
const receipt = await tx.wait();
const { proposalId } = await (0, fixture_test_1.validateProposalEvents)(governor, receipt, Number(fixture_test_1.GRANT_PROPOSAL_TYPE), proposer.address, description);
// grant owner the approver role
await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
await (0, chai_1.expect)(grantsManager.connect(voter).approveMilestones(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "NotAuthorized");
});
(0, mocha_1.it)("Milestone should be claimed by the PROPOSER once the proposal is executed and the milestone is validated", async () => {
const description = "My new project";
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("10000")];
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr( should have a revert if the amount is not equal)
"0", // deposit amount
proposer.address, milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
const balanceBeforeClaiming = await b3tr.balanceOf(proposer.address);
// try to approve with someone else than the governor roles
// grant owner the approver role
await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
await (0, chai_1.expect)(grantsManager.connect(proposer).approveMilestones(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "NotAuthorized");
// try to claim will it have not been approve by any governor role
await (0, chai_1.expect)(grantsManager.connect(proposer).claimMilestone(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "MilestoneNotApprovedByAdmin");
// grant owner the approver role
await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
// try to approve the 2nd milestone before approving the first one
await (0, chai_1.expect)(grantsManager.connect(owner).approveMilestones(proposalId, 1)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "PreviousMilestoneNotApproved");
// approve with the correct role
await grantsManager.connect(owner).approveMilestones(proposalId, 0);
// try to claim with someone else than the proposal owner
await (0, chai_1.expect)(grantsManager.connect(voter).claimMilestone(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "CallerIsNotTheGrantReceiver");
// try to claim the 2nd milestone before approving the first one
await (0, chai_1.expect)(grantsManager.connect(proposer).claimMilestone(proposalId, 1)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "MilestoneNotApprovedByAdmin");
// approve the milestone
// claim the milestone with the correct role
await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
// check the state of the milestone 0
const milestone0 = await grantsManager.getMilestone(proposalId, 0);
(0, chai_1.expect)(milestone0.isClaimed).to.equal(true); // Claimed
// approve the second milestone
await grantsManager.connect(owner).approveMilestones(proposalId, 1);
// claim the second milestone
await grantsManager.connect(proposer).claimMilestone(proposalId, 1);
const balanceAfterClaiming = await b3tr.balanceOf(proposer.address);
(0, chai_1.expect)(balanceAfterClaiming).to.equal(balanceBeforeClaiming + hardhat_1.ethers.parseEther("20000"));
});
(0, mocha_1.it)("Milestone should be claimed by the GRANTS RECEIVER once the proposal is executed and the milestone is validated", async () => {
const description = "My new project";
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("10000")];
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr( should have a revert if the amount is not equal)
"0", // deposit amount
secondaryAccount.address, milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
const balanceBeforeClaiming = await b3tr.balanceOf(secondaryAccount.address);
// try to approve with someone else than the governor roles
// grant owner the approver role
await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
await (0, chai_1.expect)(grantsManager.connect(proposer).approveMilestones(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "NotAuthorized");
// try to claim will it have not been approve by any governor role
await (0, chai_1.expect)(grantsManager.connect(secondaryAccount).claimMilestone(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "MilestoneNotApprovedByAdmin");
// grant owner the approver role
await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
// try to approve the 2nd milestone before approving the first one
await (0, chai_1.expect)(grantsManager.connect(owner).approveMilestones(proposalId, 1)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "PreviousMilestoneNotApproved");
// approve with the correct role
await grantsManager.connect(owner).approveMilestones(proposalId, 0);
// try to claim with someone else than the grants receiver
await (0, chai_1.expect)(grantsManager.connect(voter).claimMilestone(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "CallerIsNotTheGrantReceiver");
// try to claim the 2nd milestone before approving the first one
await (0, chai_1.expect)(grantsManager.connect(secondaryAccount).claimMilestone(proposalId, 1)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "MilestoneNotApprovedByAdmin");
// approve the milestone
// claim the milestone with the correct role
await grantsManager.connect(secondaryAccount).claimMilestone(proposalId, 0);
// check the state of the milestone 0
const milestone0 = await grantsManager.getMilestone(proposalId, 0);
(0, chai_1.expect)(milestone0.isClaimed).to.equal(true); // Claimed
// approve the second milestone
await grantsManager.connect(owner).approveMilestones(proposalId, 1);
// claim the second milestone
await grantsManager.connect(secondaryAccount).claimMilestone(proposalId, 1);
const balanceAfterClaiming = await b3tr.balanceOf(secondaryAccount.address);
(0, chai_1.expect)(balanceAfterClaiming).to.equal(balanceBeforeClaiming + hardhat_1.ethers.parseEther("20000"));
});
(0, mocha_1.it)("Cannot claim twice the same milestone", async () => {
const description = "My new project";
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("10000")];
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr( should have a revert if the amount is not equal)
"0", // deposit amount
proposer.address, milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
// approve the milestone
await grantsManager.connect(owner).approveMilestones(proposalId, 0);
// claim the milestone
await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
// claim the milestone again
await (0, chai_1.expect)(grantsManager.connect(proposer).claimMilestone(proposalId, 0)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "MilestoneAlreadyClaimed");
});
(0, mocha_1.it)("Should not be able to claim a milestone if the grant manager is paused", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("10000")];
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr
"0", // deposit amount
proposer.address, milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
await grantsManager.connect(owner).approveMilestones(proposalId, 0);
await grantsManager.connect(owner).pause();
await (0, chai_1.expect)(grantsManager.connect(proposer).claimMilestone(proposalId, 0)).to.be.reverted;
await grantsManager.connect(owner).unpause();
await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
});
// it("Should correctly approve / claim the milestones if more than 1 grants proposal are created")
});
(0, mocha_1.describe)("Proposal and milestone description modification", function () {
(0, mocha_1.it)("Cannot create milestone if the milestone details metadata URI is empty", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "";
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("10000")];
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const calldatas = [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]])];
(0, chai_1.expect)(governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
[0], // transferb3tr is not payable
calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "MilestoneDetailsMetadataURIEmpty");
});
(0, mocha_1.it)("Should be able to update the milestone details metadata URI", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const newMilestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qp..."; // milestones details can be changed later
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const calldatas = [
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")]),
];
// set to 1 milestone for simplicity
await grantsManager.setMinimumMilestoneCount(1);
const tx = await governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
[0], // transferb3tr is not payable
calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
const receipt = await tx.wait();
const { proposalId } = await (0, fixture_test_1.validateProposalEvents)(governor, receipt, Number(fixture_test_1.GRANT_PROPOSAL_TYPE), proposer.address, description);
await grantsManager.connect(proposer).updateMilestoneMetadataURI(proposalId, newMilestonesDetailsMetadataURI);
const grants = await grantsManager.getGrantProposal(proposalId);
(0, chai_1.expect)(grants.metadataURI).to.equal(newMilestonesDetailsMetadataURI);
});
(0, mocha_1.it)("Proposer should NOT be able to update the grants receiver", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("10000")];
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr
"0", // deposit amount
secondaryAccount.address, // grants receiver
milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
const originalGrantsReceiver = await grantsManager.getGrantsReceiverAddress(proposalId);
(0, chai_1.expect)(originalGrantsReceiver).to.equal(secondaryAccount.address);
await (0, chai_1.expect)(grantsManager.connect(proposer).updateGrantsReceiver(proposalId, owner.address)).to.be.revertedWithCustomError({
interface: grantsManagerInterface,
}, "NotAuthorized");
const grants = await grantsManager.getGrantProposal(proposalId);
(0, chai_1.expect)(grants.grantsReceiver).to.equal(originalGrantsReceiver); // Should not be updated
});
(0, mocha_1.it)("Governance should be able to update the grants receiver", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("10000")];
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr
"0", // deposit amount
proposer.address, milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
const originalGrantsReceiver = await grantsManager.getGrantsReceiverAddress(proposalId);
(0, chai_1.expect)(originalGrantsReceiver).to.equal(proposer.address);
await grantsManager.connect(owner).updateGrantsReceiver(proposalId, owner.address);
const grants = await grantsManager.getGrantProposal(proposalId);
const newGrantsReceiver = owner.address;
(0, chai_1.expect)(grants.grantsReceiver).to.equal(newGrantsReceiver); // Should be updated
(0, chai_1.expect)(newGrantsReceiver).to.not.equal(originalGrantsReceiver); // Should be different
});
(0, mocha_1.it)("Current grants receiver should be able to update the grants receiver", async () => {
const description = "My new project";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("10000")];
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr
"0", // deposit amount
secondaryAccount.address, milestonesDetailsMetadataURI, // milestones
contractToPassToMethods);
const originalGrantsReceiver = await grantsManager.getGrantsReceiverAddress(proposalId);
(0, chai_1.expect)(originalGrantsReceiver).to.equal(secondaryAccount.address);
const newGrantsReceiver = owner.address;
await grantsManager.connect(secondaryAccount).updateGrantsReceiver(proposalId, newGrantsReceiver);
const grants = await grantsManager.getGrantProposal(proposalId);
(0, chai_1.expect)(grants.grantsReceiver).to.equal(newGrantsReceiver); // Should be updated
(0, chai_1.expect)(newGrantsReceiver).to.not.equal(originalGrantsReceiver); // Should be different
});
});
(0, mocha_1.describe)("Milestone deposit", function () {
(0, mocha_1.it)("Proposer should not be able to deposits it's own grant, but can deposit for other grants", async function () {
const description = "My new project";
const description2 = "My new project 2";
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later s
const roundId = await (0, common_1.getRoundId)(contractToPassToMethods);
const tx = await governor.connect(proposer).proposeGrant([treasuryAddress, treasuryAddress], // Only Treasury for now
[0, 0], // transferb3tr is not payable
[
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")]),
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")]),
], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
const receipt = await tx.wait();
const { proposalId: proposalId1 } = await (0, fixture_test_1.validateProposalEvents)(governor, receipt, Number(fixture_test_1.GRANT_PROPOSAL_TYPE), proposer.address, description);
const tx2 = await governor.connect(owner).proposeGrant([treasuryAddress, treasuryAddress], // Only Treasury for now
[0, 0], // transferb3tr is not payable
[
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")]),
treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, hardhat_1.ethers.parseEther("1")]),
], description2, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
const receipt2 = await tx2.wait();
const { proposalId: proposalId2 } = await (0, fixture_test_1.validateProposalEvents)(governor, receipt2, Number(fixture_test_1.GRANT_PROPOSAL_TYPE), owner.address, description2);
// get the deposit amount
const depositAmount = await governor.getProposalDeposits(proposalId1);
const depositAmount2 = await governor.getProposalDeposits(proposalId2);
(0, chai_1.expect)(depositAmount).to.equal(hardhat_1.ethers.parseEther("0"));
(0, chai_1.expect)(depositAmount2).to.equal(hardhat_1.ethers.parseEther("0"));
// Proposer can deposit for other grants
await governor.connect(proposer).deposit(hardhat_1.ethers.parseEther("1"), proposalId2);
await (0, chai_1.expect)(governor.connect(proposer).deposit(hardhat_1.ethers.parseEther("1"), proposalId1)).to.be.revertedWithCustomError({
interface: governorInterface,
}, "GranteeCannotDepositOwnGrant");
const depositAmount3 = await governor.getProposalDeposits(proposalId2);
(0, chai_1.expect)(depositAmount3).to.equal(hardhat_1.ethers.parseEther("1"));
});
});
(0, mocha_1.describe)("Milestone rejection", function () {
(0, mocha_1.it)("Funds should be returned from the grants treasury to the DAO treasury if a milestone is rejected", async function () {
const description = "https://ipfs.io/ipfs/Qm..."; // project details metadata URI cannot be changed later
const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
const values = [hardhat_1.ethers.parseEther("10000"), hardhat_1.ethers.parseEther("20000")];
const granteeBalanceBeforeProposal = await b3tr.balanceOf(proposer.address);
const { proposalId } = await (0, common_1.createProposalWithMultipleFunctionsAndExecuteItGrant)(proposer, // proposer
owner, // voter
[treasury, treasury], // targets ( 2 transfers )
treasuryContract, // contract to pass to avoid re-deploying the contracts
description, // description ( will be empty in the proposal, because if modified, the proposalId and milestoneId will be modified => lost in the see)
["transferB3TR", "transferB3TR"], // functionToCall
[
[grantsManagerAddress, values[0]],
[grantsManagerAddress, values[1]],
], // args of transferb3tr
0, proposer.address, milestonesDetailsMetadataURI, // milestones
contract