UNPKG

@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
"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