UNPKG

@vechain/vebetterdao-contracts

Version:

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

984 lines 71.3 kB
import { describe, it, beforeEach } from "mocha";
import { setupProposer, validateProposalEvents, setupGovernanceFixtureWithEmissions, GRANT_PROPOSAL_TYPE, setupVoter, } from "../fixture.test";
import { GrantsManager__factory, GovernorProposalLogic__factory, B3TRGovernor__factory, } from "../../../typechain-types";
import { ethers } from "hardhat";
import { expect } from "chai";
import { createGrantProposal, createProposalWithMultipleFunctionsAndExecuteItGrant, getRoundId, moveBlocks, payDeposit, waitForCurrentRoundToEnd, } from "../../helpers/common";
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;
    beforeEach(async function () {
        const fixture = await 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 setupProposer(proposer, b3tr, vot3, minterAccount);
        await vot3.connect(proposer).approve(await governor.getAddress(), ethers.parseEther("1000"));
        grantsManagerAddress = await grantsManager.getAddress();
        treasuryAddress = await treasury.getAddress();
        treasuryContract = await ethers.getContractFactory("Treasury");
        contractToPassToMethods = {
            b3tr,
            vot3,
            minterAccount,
            governor,
            treasury,
            emissions,
            xAllocationVoting,
            veBetterPassport,
            owner,
            timeLock,
            grantsManager,
        };
        grantsManagerInterface = GrantsManager__factory.createInterface();
        governorProposalLogicInterface = GovernorProposalLogic__factory.createInterface();
        governorInterface = B3TRGovernor__factory.createInterface();
    });
    describe("Milestone contract setup and creation", function () {
        it("Should set the minimum milestone count", async function () {
            const minimumMilestoneCount = await grantsManager.getMinimumMilestoneCount();
            expect(minimumMilestoneCount).to.equal(2); // MINIMUM_MILESTONE_COUNT = 2
        });
        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);
            (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();
            expect(storedGovernor).to.equal(governorProxyAddress, "GrantsManager should have the correct governor address");
        });
        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 = [ethers.parseEther("10000")];
            const roundId = await getRoundId(contractToPassToMethods);
            const calldatas = [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]])];
            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");
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const roundId = await getRoundId(contractToPassToMethods);
            const calldatas = [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]])];
            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");
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const roundId = await getRoundId(contractToPassToMethods);
            const calldatas = [
                treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]]),
                treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[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");
        });
        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 = [ethers.parseEther("10000"), 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 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 validateProposalEvents(governor, receipt, Number(GRANT_PROPOSAL_TYPE), proposer.address, description);
            // Verify milestone data was created during proposal creation
            const storedGrant = await grantsManager.getGrantProposal(proposalId);
            expect(storedGrant.totalAmount).to.equal(totalAmount);
            expect(storedGrant.proposer).to.equal(proposer.address);
            expect(storedGrant.milestones.length).to.equal(calldatas.length);
            expect(storedGrant.milestones[0].amount).to.equal(values[0]);
            expect(storedGrant.milestones[1].amount).to.equal(values[1]);
            expect(storedGrant.milestones[0].isClaimed).to.equal(false);
            expect(storedGrant.milestones[1].isClaimed).to.equal(false);
            expect(storedGrant.milestones[0].isApproved).to.equal(false);
            expect(storedGrant.milestones[1].isApproved).to.equal(false);
            expect(storedGrant.milestones[0].isRejected).to.equal(false);
            expect(storedGrant.milestones[1].isRejected).to.equal(false);
            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);
            expect(firstMilestoneState).to.equal(0); // Pending
            expect(secondMilestoneState).to.equal(0); // Pending
            // get the grant state
            const grantState = await grantsManager.grantState(proposalId);
            expect(grantState).to.equal(0); // Pending state <=> the proposal state
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            // get the roundId
            const roundId = await getRoundId(contractToPassToMethods);
            const calldatas = [treasury.interface.encodeFunctionData("transferVET", [grantsManagerAddress, values[0]])];
            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");
        });
        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 = [ethers.parseEther("0")];
            const roundId = await getRoundId(contractToPassToMethods);
            const calldatas = [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]])];
            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");
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const roundId = await 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]]),
            ];
            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");
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            await governor.setMinVotingDelay(3); // normally set to 1
            await moveBlocks(17);
            let currentBlock = await governor.clock();
            let currentRoundsEndsAt = await xAllocationVoting.currentRoundDeadline();
            let minVotingDelay = await governor.minVotingDelay();
            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
            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 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();
            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);
        });
        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 expect(grantsManager
                .connect(owner)
                .createMilestones(milestonesDetailsMetadataURI, 0, proposer.address, owner.address, [], {})).to.be.revertedWithCustomError({
                interface: grantsManagerInterface,
            }, "NotAuthorized");
        });
    });
    describe("Milestone execution", function () {
        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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const { proposalId } = await 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);
            expect(totalAmountFromProposal).to.equal(values.reduce((a, b) => a + b, 0n));
            expect(await b3tr.balanceOf(grantsManagerAddress)).to.equal(totalAmountFromProposal);
        });
        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, ethers.parseEther("1")]),
                treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")]),
            ];
            const roundId = await 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 validateProposalEvents(governor, receipt, Number(GRANT_PROPOSAL_TYPE), proposer.address, description);
            const milestoneStatus = await grantsManager.milestoneState(proposalId, 0);
            expect(milestoneStatus).to.equal(0);
        });
        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 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, ethers.parseEther("1")],
                [grantsManagerAddress, 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 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
            expect(milestonseRegistered.isApproved).to.equal(true); // Approved
            expect(milestonseRegistered.amount).to.equal(ethers.parseEther("1"));
            // other milestone should be pending
            const milestonseRegistered2 = await grantsManager.getMilestone(proposalId, 1); // the second one should be pending
            expect(milestonseRegistered2.isApproved).to.equal(false); // Pending
        });
        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 getRoundId(contractToPassToMethods);
            const tx = await governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
            [0], // transferb3tr is not payable
            [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")])], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
            const receipt = await tx.wait();
            const { proposalId } = await validateProposalEvents(governor, receipt, Number(GRANT_PROPOSAL_TYPE), proposer.address, description);
            const milestone = await grantsManager.getMilestone(proposalId, 0);
            expect(milestone.isApproved).to.equal(false); // Pending
            expect(milestone.isClaimed).to.equal(false); // Pending
            // grant owner the approver role
            await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
            await expect(grantsManager.connect(owner).approveMilestones(proposalId, 0)).to.be.revertedWithCustomError({
                interface: grantsManagerInterface,
            }, "ProposalNotExecuted");
        });
        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 getRoundId(contractToPassToMethods);
            const tx = await governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
            [0], // transferb3tr is not payable
            [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")])], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
            const receipt = await tx.wait();
            const { proposalId } = await validateProposalEvents(governor, receipt, Number(GRANT_PROPOSAL_TYPE), proposer.address, description);
            // grant owner the approver role
            await grantsManager.grantRole(await grantsManager.GRANTS_APPROVER_ROLE(), owner.address);
            await expect(grantsManager.connect(voter).approveMilestones(proposalId, 0)).to.be.revertedWithCustomError({
                interface: grantsManagerInterface,
            }, "NotAuthorized");
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("10000")];
            const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
            const { proposalId } = await 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 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 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 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 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 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);
            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);
            expect(balanceAfterClaiming).to.equal(balanceBeforeClaiming + ethers.parseEther("20000"));
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("10000")];
            const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
            const { proposalId } = await 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 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 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 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 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 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);
            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);
            expect(balanceAfterClaiming).to.equal(balanceBeforeClaiming + ethers.parseEther("20000"));
        });
        it("Cannot claim twice the same milestone", async () => {
            const description = "My new project";
            const values = [ethers.parseEther("10000"), ethers.parseEther("10000")];
            const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later
            const { proposalId } = await 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 expect(grantsManager.connect(proposer).claimMilestone(proposalId, 0)).to.be.revertedWithCustomError({
                interface: grantsManagerInterface,
            }, "MilestoneAlreadyClaimed");
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("10000")];
            const { proposalId } = await 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 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")
    });
    describe("Proposal and milestone description modification", function () {
        it("Cannot create milestone if the milestone details metadata URI is empty", async () => {
            const description = "My new project";
            const milestonesDetailsMetadataURI = "";
            const values = [ethers.parseEther("10000"), ethers.parseEther("10000")];
            const roundId = await getRoundId(contractToPassToMethods);
            const calldatas = [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, values[0]])];
            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");
        });
        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 getRoundId(contractToPassToMethods);
            const calldatas = [
                treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, 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 validateProposalEvents(governor, receipt, Number(GRANT_PROPOSAL_TYPE), proposer.address, description);
            await grantsManager.connect(proposer).updateMilestoneMetadataURI(proposalId, newMilestonesDetailsMetadataURI);
            const grants = await grantsManager.getGrantProposal(proposalId);
            expect(grants.metadataURI).to.equal(newMilestonesDetailsMetadataURI);
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("10000")];
            const { proposalId } = await 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);
            expect(originalGrantsReceiver).to.equal(secondaryAccount.address);
            await expect(grantsManager.connect(proposer).updateGrantsReceiver(proposalId, owner.address)).to.be.revertedWithCustomError({
                interface: grantsManagerInterface,
            }, "NotAuthorized");
            const grants = await grantsManager.getGrantProposal(proposalId);
            expect(grants.grantsReceiver).to.equal(originalGrantsReceiver); // Should not be updated
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("10000")];
            const { proposalId } = await 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);
            expect(originalGrantsReceiver).to.equal(proposer.address);
            await grantsManager.connect(owner).updateGrantsReceiver(proposalId, owner.address);
            const grants = await grantsManager.getGrantProposal(proposalId);
            const newGrantsReceiver = owner.address;
            expect(grants.grantsReceiver).to.equal(newGrantsReceiver); // Should be updated
            expect(newGrantsReceiver).to.not.equal(originalGrantsReceiver); // Should be different
        });
        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 = [ethers.parseEther("10000"), ethers.parseEther("10000")];
            const { proposalId } = await 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);
            expect(originalGrantsReceiver).to.equal(secondaryAccount.address);
            const newGrantsReceiver = owner.address;
            await grantsManager.connect(secondaryAccount).updateGrantsReceiver(proposalId, newGrantsReceiver);
            const grants = await grantsManager.getGrantProposal(proposalId);
            expect(grants.grantsReceiver).to.equal(newGrantsReceiver); // Should be updated
            expect(newGrantsReceiver).to.not.equal(originalGrantsReceiver); // Should be different
        });
    });
    describe("Milestone deposit", function () {
        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 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, ethers.parseEther("1")]),
                treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")]),
            ], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
            const receipt = await tx.wait();
            const { proposalId: proposalId1 } = await validateProposalEvents(governor, receipt, Number(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, ethers.parseEther("1")]),
                treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")]),
            ], description2, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
            const receipt2 = await tx2.wait();
            const { proposalId: proposalId2 } = await validateProposalEvents(governor, receipt2, Number(GRANT_PROPOSAL_TYPE), owner.address, description2);
            // get the deposit amount
            const depositAmount = await governor.getProposalDeposits(proposalId1);
            const depositAmount2 = await governor.getProposalDeposits(proposalId2);
            expect(depositAmount).to.equal(ethers.parseEther("0"));
            expect(depositAmount2).to.equal(ethers.parseEther("0"));
            // Proposer can deposit for other grants
            await governor.connect(proposer).deposit(ethers.parseEther("1"), proposalId2);
            await expect(governor.connect(proposer).deposit(ethers.parseEther("1"), proposalId1)).to.be.revertedWithCustomError({
                interface: governorInterface,
            }, "GranteeCannotDepositOwnGrant");
            const depositAmount3 = await governor.getProposalDeposits(proposalId2);
            expect(depositAmount3).to.equal(ethers.parseEther("1"));
        });
    });
    describe("Milestone rejection", function () {
        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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const granteeBalanceBeforeProposal = await b3tr.balanceOf(proposer.address);
            const { proposalId } = await 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
            contractToPassToMethods);
            // Balances after the proposal
            const treasuryBalanceAfterProposalExecuted = await b3tr.balanceOf(treasuryAddress); // - 30000 (removed from the DAO's treasury to the grants manager)
            const grantsManagerBalanceAfterProposalExecuted = await b3tr.balanceOf(grantsManagerAddress); // + 30000
            // approve + claim, the grantee should have 10000
            const granteeClaimingAmount = ethers.parseEther("10000");
            await grantsManager.connect(owner).approveMilestones(proposalId, 0);
            await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
            const granteeBalanceBeforeRejection = await b3tr.balanceOf(proposer.address);
            expect(granteeBalanceBeforeRejection).to.equal(granteeBalanceBeforeProposal + granteeClaimingAmount);
            const grantsManagerBalanceAfterGranteeClaiming = await b3tr.balanceOf(grantsManagerAddress);
            expect(grantsManagerBalanceAfterGranteeClaiming).to.equal(grantsManagerBalanceAfterProposalExecuted - granteeClaimingAmount);
            // Should transfer back to the DAO's treasury the remaingin funds ( 10 000 )
            await grantsManager.grantRole(await grantsManager.GRANTS_REJECTOR_ROLE(), owner.address);
            await expect(grantsManager.connect(owner).rejectMilestones(proposalId)).to.emit(grantsManager, "MilestoneRejectedAndFundsReturnedToTreasury");
            // Check the states ( Grant should be Canceled, Milestone1 should be Claimed, Milestone2 should be Rejected )
            const grantState = await grantsManager.grantState(proposalId);
            expect(grantState).to.equal(2); // Canceled
            const milestoneState = await grantsManager.milestoneState(proposalId, 0);
            expect(milestoneState).to.equal(2n); // Claimed
            const milestoneState2 = await grantsManager.milestoneState(proposalId, 1);
            expect(milestoneState2).to.equal(3n); // Rejected
            const isGrantRejected = await grantsManager.isGrantRejected(proposalId);
            expect(isGrantRejected).to.equal(true);
            const grantsManagerBalanceAfterRejection = await b3tr.balanceOf(grantsManagerAddress);
            expect(grantsManagerBalanceAfterRejection).to.equal(0);
        });
        it("Funds should be returned from the grants treasury to the DAO treasury if a grant is rejected, the already claimed funds should not be returned", 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 = [ethers.parseEther("10000"), ethers.parseEther("20000"), ethers.parseEther("30000")];
            const granteeBalanceBeforeProposal = await b3tr.balanceOf(proposer.address);
            const { proposalId } = await 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", "transferB3TR"], // functionToCall
            [
                [grantsManagerAddress, values[0]],
                [grantsManagerAddress, values[1]],
                [grantsManagerAddress, values[2]],
            ], // args of transferb3tr
            0, proposer.address, milestonesDetailsMetadataURI, // milestones
            contractToPassToMethods);
            // Balances after the proposal
            const grantsManagerBalanceAfterProposalExecuted = await b3tr.balanceOf(grantsManagerAddress); // + 30000
            // Balances before the rejection
            const treasuryBalanceBeforeRejection = await b3tr.balanceOf(treasuryAddress);
            // approve + claim, the grantee should have 10000
            const granteeClaimingAmount = ethers.parseEther("10000");
            await grantsManager.connect(owner).approveMilestones(proposalId, 0);
            await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
            const granteeBalanceBeforeRejection = await b3tr.balanceOf(proposer.address);
            expect(granteeBalanceBeforeRejection).to.equal(granteeBalanceBeforeProposal + granteeClaimingAmount);
            const grantsManagerBalanceAfterGranteeClaiming = await b3tr.balanceOf(grantsManagerAddress);
            expect(grantsManagerBalanceAfterGranteeClaiming).to.equal(grantsManagerBalanceAfterProposalExecuted - granteeClaimingAmount);
            // Should transfer back to the DAO's treasury the remaingin funds ( 10 000 )
            await grantsManager.grantRole(await grantsManager.GRANTS_REJECTOR_ROLE(), owner.address);
            await expect(grantsManager.connect(owner).rejectMilestones(proposalId)).to.emit(grantsManager, "MilestoneRejectedAndFundsReturnedToTreasury");
            // Check the states ( Grant should be Canceled, Milestone1 should be Claimed, Milestone2 should be Rejected )
            const grantState = await grantsManager.grantState(proposalId);
            expect(grantState).to.equal(2); // Canceled
            const milestoneState = await grantsManager.milestoneState(proposalId, 0);
            expect(milestoneState).to.equal(2n); // Claimed
            const milestoneState2 = await grantsManager.milestoneState(proposalId, 1);
            expect(milestoneState2).to.equal(3n); // Rejected
            const milestoneState3 = await grantsManager.milestoneState(proposalId, 2);
            expect(milestoneState3).to.equal(3n); // Rejected
            const isGrantRejected = await grantsManager.isGrantRejected(proposalId);
            expect(isGrantRejected).to.equal(true);
            const grantsManagerBalanceAfterRejection = await b3tr.balanceOf(grantsManagerAddress);
            expect(grantsManagerBalanceAfterRejection).to.equal(0);
            //Current treasury balance should be the sum of the milestones 2 and 3 and past balance
            const currentTreasuryBalance = await b3tr.balanceOf(treasuryAddress);
            expect(currentTreasuryBalance).to.be.eq(treasuryBalanceBeforeRejection + values[1] + values[2]);
        });
    });
    describe("Milestone states", function () {
        it("If all the milestone are approved, the proposal should be completed", 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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const { proposalId } = await 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
            contractToPassToMethods);
            // Approve the first milestone
            await grantsManager.connect(owner).approveMilestones(proposalId, 0);
            // Approve the second milestone
            await grantsManager.connect(owner).approveMilestones(proposalId, 1);
            // Claim the first milestone
            await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
            // Claim the second milestone
            await grantsManager.connect(proposer).claimMilestone(proposalId, 1);
            // Check the states ( Proposal should be Executed, Milestone should be Claimed )
            const grantState = await grantsManager.grantState(proposalId);
            expect(grantState).to.equal(9); // Completed ( all funds have been sent )
            const milestoneState = await grantsManager.milestoneState(proposalId, 0);
            expect(milestoneState).to.equal(2n); // Claimed
            const milestoneState2 = await grantsManager.milestoneState(proposalId, 1);
            expect(milestoneState2).to.equal(2n); // Claimed
            const isGrantCompleted = await grantsManager.isGrantCompleted(proposalId);
            expect(isGrantCompleted).to.equal(true);
        });
        it("If a milestone is rejected, the grant should be canceled", 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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const { proposalId } = await 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
            contractToPassToMethods);
            // Approve the first milestone
            await grantsManager.connect(owner).approveMilestones(proposalId, 0);
            // Claim the first milestone
            await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
            // Reject the second milestone
            await grantsManager.connect(owner).rejectMilestones(proposalId);
            // Check the states ( Proposal should be Canceled, Milestone1 should be Claimed, Milestone2 should be Rejected )
            const grantState = await grantsManager.grantState(proposalId);
            expect(grantState).to.equal(2); // Canceled
            const milestoneState = await grantsManager.milestoneState(proposalId, 0);
            expect(milestoneState).to.equal(2n); // Claimed
            const milestoneState2 = await grantsManager.milestoneState(proposalId, 1);
            expect(milestoneState2).to.equal(3n); // Rejected
            const isGrantRejected = await grantsManager.isGrantRejected(proposalId);
            expect(isGrantRejected).to.equal(true);
        });
        it("If any milestone is still pending, the grant should be in development", 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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const { proposalId } = await 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
            contractToPassToMethods);
            // Approve the first milestone
            await grantsManager.connect(owner).approveMilestones(proposalId, 0);
            // Claim the first milestone
            await grantsManager.connect(proposer).claimMilestone(proposalId, 0); // Claim the first milestone
            // Check the states ( Proposal should be InDevelopment, Milestone1 should be Claimed, Milestone2 should be Pending )
            const grantState = await grantsManager.grantState(proposalId);
            expect(grantState).to.equal(8); // InDevelopment
            const milestoneState = await grantsManager.milestoneState(proposalId, 0);
            expect(milestoneState).to.equal(2n); // Claimed
            const milestoneState2 = await grantsManager.milestoneState(proposalId, 1);
            expect(milestoneState2).to.equal(0n); // Pending
            const isGrantInDevelopment = await grantsManager.isGrantInDevelopment(proposalId);
            expect(isGrantInDevelopment).to.equal(true);
        });
        it("Grant should be in developement if the proposal is executed and at least one milestone is pending", 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 = [ethers.parseEther("10000"), ethers.parseEther("20000")];
            const { proposalId } = await 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
            contractToPassToMethods);
            // Approve the first milestone
            await grantsManager.connect(owner).approveMilestones(proposalId, 0);
            // Claim the first milestone
            await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
            const grantState = await grantsManager.grantState(proposalId);
            expect(grantState).to.equal(8); // InDevelopment
            const milestoneState = await grantsManager.milestoneState(proposalId, 0);
            expect(milestoneState).to.equal(2n); // Claimed
            const milestoneState2 = await grantsManager.milestoneState(proposalId, 1);
            expect(milestoneState2).to.equal(0n); // Pending
            const isGrantCompleted = await grantsManager.isGrantCompleted(proposalId);
            expect(isGrantCompleted).to.equal(false);
            const isGrantInDevelopment = await grantsManager.isGrantInDevelopment(proposalId);
            expect(isGrantInDevelopment).to.equal(true);
        });
        it("Grant should neither be in development nor completed if the proposal is not executed ( the funds are not yet transferred to the grants manager )", 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 roundId = await getRoundId(contractToPassToMethods);
            await grantsManager.setMinimumMilestoneCount(1);
            const tx = await governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
            [0], // transferb3tr is not payable
            [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")])], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
            const receipt = await tx.wait();
            const { proposalId } = await validateProposalEvents(governor, receipt, Number(GRANT_PROPOSAL_TYPE), proposer.address, description);
            const grantState = await grantsManager.grantState(proposalId);
            expect(grantState).to.equal(0); // Pending
            const milestoneState = await grantsManager.milestoneState(proposalId, 0);
            expect(milestoneState).to.equal(0n); // Pending
            const isGrantCompleted = await grantsManager.isGrantCompleted(proposalId);
            expect(isGrantCompleted).to.equal(false);
            const isGrantInDevelopment = await grantsManager.isGrantInDevelopment(proposalId);
            expect(isGrantInDevelopment).to.equal(false);
        });
        it("Until it is executed , grants manager and b3tr governor state should be exacly the same", async function () {
            // Setup voter
            await setupVoter(voter, b3tr, vot3, minterAccount, owner, veBetterPassport);
            const description = "My new project";
            const milestonesDetailsMetadataURI = "https://ipfs.io/ipfs/Qm..."; // milestones details can be changed later s
            const targets = [treasuryAddress, treasuryAddress];
            const values = [0, 0];
            const calldatas = [
                treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")]),
                treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")]),
            ];
            const roundId = await getRoundId(contractToPassToMethods);
            const tx = await governor.connect(proposer).proposeGrant(targets, // Only Treasury for now
            values, // transferb3tr is not payable
            calldatas, description, roundId, 0, proposer.address, milestonesDetailsMetadataURI);
            const receipt = await tx.wait();
            const { proposalId } = await validateProposalEvents(governor, receipt, Number(GRANT_PROPOSAL_TYPE), proposer.address, description);
            // Check the states ( Proposal should be Pending and Grant should be Pending )
            expect(await governor.state(proposalId)).to.equal(await grantsManager.grantState(proposalId));
            //Pay the deposit
            await payDeposit(proposalId, owner);
            //Check the states ( Proposal should be Des and Grant should be Pending )
            expect(await governor.state(proposalId)).to.equal(await grantsManager.grantState(proposalId));
            // simulate start of new round with enough voting delay
            await waitForCurrentRoundToEnd();
            await emissions.distribute();
            //Check the states ( Proposal should be active and Grant should be active )
            expect(await governor.state(proposalId)).to.equal(await grantsManager.grantState(proposalId));
            await governor.connect(voter).castVote(proposalId, 1, { gasLimit: 10000000 }); // vote for
            // simulate start of new round with enough voting delay
            await waitForCurrentRoundToEnd();
            await emissions.distribute();
            //Check the states ( Proposal should be Succeeded and Grant should be Succeeded )
            expect(await governor.state(proposalId)).to.equal(await grantsManager.grantState(proposalId));
            // Queue the proposal
            const descriptionHash = ethers.keccak256(ethers.toUtf8Bytes(description));
            await governor.connect(proposer).queue(targets, values, calldatas, descriptionHash);
            //Check the states ( Proposal should be Queued and Grant should be Queued )
            expect(await governor.state(proposalId)).to.equal(await grantsManager.grantState(proposalId));
            // Execute the proposal
            await governor.connect(owner).execute(targets, values, calldatas, descriptionHash);
            //Check the states ( Proposal should be Executed and Grant should be InDevelopment )
            expect(await governor.state(proposalId)).to.equal("6");
            expect(await grantsManager.grantState(proposalId)).to.equal("8");
            //Approve first milestone
            await grantsManager.connect(owner).approveMilestones(proposalId, 0);
            // Claim first milestone
            await grantsManager.connect(proposer).claimMilestone(proposalId, 0);
            //Governor state should remain executed and grant state should remain in development
            expect(await governor.state(proposalId)).to.equal("6");
            expect(await grantsManager.grantState(proposalId)).to.equal("8");
            //Approve second milestone (this is the last one)
            await grantsManager.connect(owner).approveMilestones(proposalId, 1);
            // Claim second milestone
            await grantsManager.connect(proposer).claimMilestone(proposalId, 1);
            //Governor state should remain executed and grant state should be completed
            expect(await governor.state(proposalId)).to.equal("6");
            expect(await grantsManager.grantState(proposalId)).to.equal("9");
        });
    });
    describe("Pause and unpause", function () {
        it("Only pauser role or governance should be able to pause and unpause the grants manager", async function () {
            await expect(grantsManager.connect(proposer).pause()).to.be.revertedWithCustomError({
                interface: grantsManagerInterface,
            }, "NotAuthorized");
            await grantsManager.grantRole(await grantsManager.PAUSER_ROLE(), proposer.address);
            await expect(grantsManager.connect(proposer).pause()).to.emit(grantsManager, "Paused");
            await expect(grantsManager.connect(proposer).unpause()).to.emit(grantsManager, "Unpaused");
        });
        it("Should not be able to create a proposal if the grant manager is paused", 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 roundId = await getRoundId(contractToPassToMethods);
            await grantsManager.setMinimumMilestoneCount(1);
            await grantsManager.connect(owner).pause();
            await expect(governor.connect(proposer).proposeGrant([treasuryAddress], // Only Treasury for now
            [0], // transferb3tr is not payable
            [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")])], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.be.reverted;
            await grantsManager.connect(owner).unpause();
            expect(await governor
                .connect(proposer)
                .proposeGrant([treasuryAddress], [0], [treasury.interface.encodeFunctionData("transferB3TR", [grantsManagerAddress, ethers.parseEther("1")])], description, roundId, 0, proposer.address, milestonesDetailsMetadataURI)).to.emit(governor, "ProposalCreated");
        });
    });
});