diff --git a/packages/contracts/hardhat.config.ts b/packages/contracts/hardhat.config.ts index ea426fc..09a90f7 100644 --- a/packages/contracts/hardhat.config.ts +++ b/packages/contracts/hardhat.config.ts @@ -1,39 +1,39 @@ -import {NetworkNameMapping} from './utils/helpers'; -import '@nomicfoundation/hardhat-chai-matchers'; -import '@nomicfoundation/hardhat-toolbox'; -import '@nomiclabs/hardhat-etherscan'; -import '@openzeppelin/hardhat-upgrades'; -import '@typechain/hardhat'; -import {config as dotenvConfig} from 'dotenv'; -import 'hardhat-deploy'; -import 'hardhat-gas-reporter'; -import {extendEnvironment, HardhatUserConfig} from 'hardhat/config'; -import {HardhatRuntimeEnvironment} from 'hardhat/types'; -import type {NetworkUserConfig} from 'hardhat/types'; -import {resolve} from 'path'; -import 'solidity-coverage'; +import { NetworkNameMapping } from "./utils/helpers"; +import "@nomicfoundation/hardhat-chai-matchers"; +import "@nomicfoundation/hardhat-toolbox"; +import "@nomiclabs/hardhat-etherscan"; +import "@openzeppelin/hardhat-upgrades"; +import "@typechain/hardhat"; +import { config as dotenvConfig } from "dotenv"; +import "hardhat-deploy"; +import "hardhat-gas-reporter"; +import { extendEnvironment, HardhatUserConfig } from "hardhat/config"; +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import type { NetworkUserConfig } from "hardhat/types"; +import { resolve } from "path"; +import "solidity-coverage"; -const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || '../../.env'; -dotenvConfig({path: resolve(__dirname, dotenvConfigPath)}); +const dotenvConfigPath: string = process.env.DOTENV_CONFIG_PATH || "../../.env"; +dotenvConfig({ path: resolve(__dirname, dotenvConfigPath) }); if (!process.env.INFURA_API_KEY) { - throw new Error('INFURA_API_KEY in .env not set'); + throw new Error("INFURA_API_KEY in .env not set"); } const apiUrls: NetworkNameMapping = { - mainnet: 'https://mainnet.infura.io/v3/', - goerli: 'https://goerli.infura.io/v3/', - polygon: 'https://polygon-mainnet.infura.io/v3/', - polygonMumbai: 'https://polygon-mumbai.infura.io/v3/', - baseGoerli: 'https://goerli.base.org', + mainnet: "https://mainnet.infura.io/v3/", + goerli: "https://goerli.infura.io/v3/", + polygon: "https://polygon-mainnet.infura.io/v3/", + polygonMumbai: "https://polygon-mumbai.infura.io/v3/", + baseGoerli: "https://goerli.base.org", }; -export const networks: {[index: string]: NetworkUserConfig} = { +export const networks: { [index: string]: NetworkUserConfig } = { hardhat: { chainId: 31337, forking: { url: `${ - apiUrls[process.env.NETWORK_NAME ? process.env.NETWORK_NAME : 'mainnet'] + apiUrls[process.env.NETWORK_NAME ? process.env.NETWORK_NAME : "mainnet"] }${process.env.INFURA_API_KEY}`, }, }, @@ -62,14 +62,14 @@ export const networks: {[index: string]: NetworkUserConfig} = { // Uses hardhats private key if none is set. DON'T USE THIS ACCOUNT FOR DEPLOYMENTS const accounts = process.env.PRIVATE_KEY - ? process.env.PRIVATE_KEY.split(',') - : ['0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80']; + ? process.env.PRIVATE_KEY.split(",") + : ["0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80"]; for (const network in networks) { // special treatement for hardhat - if (network.startsWith('hardhat')) { + if (network.startsWith("hardhat")) { networks[network].accounts = { - mnemonic: 'test test test test test test test test test test test junk', + mnemonic: "test test test test test test test test test test test junk", }; continue; } @@ -82,26 +82,29 @@ extendEnvironment((hre: HardhatRuntimeEnvironment) => { }); const config: HardhatUserConfig = { - defaultNetwork: 'hardhat', + defaultNetwork: "hardhat", etherscan: { apiKey: { - mainnet: process.env.ETHERSCAN_API_KEY || '', - goerli: process.env.ETHERSCAN_API_KEY || '', - polygon: process.env.POLYGONSCAN_API_KEY || '', - polygonMumbai: process.env.POLYGONSCAN_API_KEY || '', - baseGoerli: process.env.BASESCAN_API_KEY || '', + mainnet: process.env.ETHERSCAN_API_KEY || "", + goerli: process.env.ETHERSCAN_API_KEY || "", + polygon: process.env.POLYGONSCAN_API_KEY || "", + polygonMumbai: process.env.POLYGONSCAN_API_KEY || "", + baseGoerli: process.env.BASESCAN_API_KEY || "", }, customChains: [ { - network: 'baseGoerli', + network: "baseGoerli", chainId: 84531, urls: { - apiURL: 'https://api-goerli.basescan.org/api', - browserURL: 'https://goerli.basescan.org', + apiURL: "https://api-goerli.basescan.org/api", + browserURL: "https://goerli.basescan.org", }, }, ], }, + mocha: { + timeout: 90 * 1000, + }, namedAccounts: { deployer: 0, @@ -119,28 +122,28 @@ const config: HardhatUserConfig = { }, gasReporter: { - currency: 'USD', - enabled: process.env.REPORT_GAS === 'true' ? true : false, + currency: "USD", + enabled: process.env.REPORT_GAS === "true" ? true : false, excludeContracts: [], - src: './contracts', + src: "./contracts", coinmarketcap: process.env.COINMARKETCAP_API_KEY, }, networks, paths: { - artifacts: './artifacts', - cache: './cache', - sources: './src', - tests: './test', - deploy: './deploy', + artifacts: "./artifacts", + cache: "./cache", + sources: "./src", + tests: "./test", + deploy: "./deploy", }, solidity: { - version: '0.8.17', + version: "0.8.17", settings: { metadata: { // Not including the metadata hash // https://github.com/paulrberg/hardhat-template/issues/31 - bytecodeHash: 'none', + bytecodeHash: "none", }, // Disable the optimizer when debugging // https://hardhat.org/hardhat-network/#solidity-optimizer-support @@ -151,8 +154,8 @@ const config: HardhatUserConfig = { }, }, typechain: { - outDir: 'typechain', - target: 'ethers-v5', + outDir: "typechain", + target: "ethers-v5", }, }; diff --git a/packages/contracts/test/unit-testing/member-access-plugin.ts b/packages/contracts/test/unit-testing/member-access-plugin.ts index db97304..c1d252a 100644 --- a/packages/contracts/test/unit-testing/member-access-plugin.ts +++ b/packages/contracts/test/unit-testing/member-access-plugin.ts @@ -22,6 +22,7 @@ import { EMPTY_DATA, EXECUTE_PERMISSION_ID, MEMBER_PERMISSION_ID, + mineBlock, ROOT_PERMISSION_ID, UPDATE_ADDRESSES_PERMISSION_ID, UPDATE_MULTISIG_SETTINGS_PERMISSION_ID, @@ -46,8 +47,8 @@ export const defaultInitData: InitData = { }; export const multisigInterface = new ethers.utils.Interface([ - "function initialize(address,address[],tuple(bool,uint16))", - "function updateMultisigSettings(tuple(bool,uint16))", + "function initialize(address,tuple(uint64,address))", + "function updateMultisigSettings(tuple(uint64,address))", "function proposeNewMember(bytes,address)", "function proposeRemoveMember(bytes,address)", "function getProposal(uint256)", @@ -1522,7 +1523,7 @@ describe("Member Access Plugin", function () { await ethers.provider.send("evm_setAutomine", [false]); await dao.execute( - "0x00", + ZERO_BYTES32, [ { to: memberAccessPlugin.address, @@ -1546,25 +1547,32 @@ describe("Member Access Plugin", function () { memberAccessPlugin, "ProposalCreationForbidden", ) - .withArgs(charlie.address); + .withArgs(alice.address); await ethers.provider.send("evm_setAutomine", [true]); }); }); - describe("canApprove:", async () => { - let id = 1; + describe("canApprove:", () => { + let id = 0; beforeEach(async () => { await proposeNewEditor(bob.address); // have 2 editors + await mineBlock(); + + expect(await memberAccessPlugin.isEditor(alice.address)).to.be.true; + expect(await memberAccessPlugin.isEditor(bob.address)).to.be.true; + expect(await memberAccessPlugin.isEditor(charlie.address)).to.be.false; // Alice approves await memberAccessPlugin.proposeNewMember(EMPTY_DATA, charlie.address); - id = 1; + id = 0; }); it("returns `false` if the proposal is already executed", async () => { - expect((await memberAccessPlugin.getProposal(id)).executed).to.be.true; + expect((await memberAccessPlugin.getProposal(id)).executed).to.be.false; + await memberAccessPlugin.connect(bob).approve(id, false); + expect((await memberAccessPlugin.getProposal(id)).executed).to.be.true; expect(await memberAccessPlugin.canApprove(id, signers[3].address)).to .be.false; }); @@ -1591,28 +1599,28 @@ describe("Member Access Plugin", function () { }); it("returns `false` if the proposal has ended", async () => { - await proposeNewEditor(bob.address); // have 2 editors await memberAccessPlugin.proposeNewMember(EMPTY_DATA, charlie.address); - id = 2; + id++; expect(await memberAccessPlugin.canApprove(id, bob.address)).to .be.true; - await memberAccessPlugin.approve(id, false); + await memberAccessPlugin.connect(bob).approve(id, false); expect(await memberAccessPlugin.canApprove(id, bob.address)).to .be.false; }); }); - describe("hasApproved", async () => { - let id = 1; + describe("hasApproved", () => { + let id = 0; beforeEach(async () => { await proposeNewEditor(bob.address); // have 2 editors + await mineBlock(); // Alice approves await memberAccessPlugin.proposeNewMember(EMPTY_DATA, charlie.address); - id = 1; + id = 0; }); it("returns `false` if user hasn't approved yet", async () => { @@ -1627,31 +1635,32 @@ describe("Member Access Plugin", function () { }); }); - describe("approve:", async () => { - let id = 1; + describe("approve:", () => { + let id = 0; beforeEach(async () => { await proposeNewEditor(bob.address); // have 2 editors + await mineBlock(); // Alice approves await memberAccessPlugin.proposeNewMember(EMPTY_DATA, charlie.address); - id = 1; + id = 0; }); it("reverts when approving multiple times", async () => { - await memberAccessPlugin.approve(id, true); + await memberAccessPlugin.connect(bob).approve(id, true); // Try to vote again - await expect(memberAccessPlugin.approve(id, true)) + await expect(memberAccessPlugin.connect(bob).approve(id, true)) .to.be.revertedWithCustomError( memberAccessPlugin, "ApprovalCastForbidden", ) - .withArgs(id, signers[0].address); + .withArgs(id, bob.address); }); it("reverts if minimal approval is not met yet", async () => { const proposal = await memberAccessPlugin.getProposal(id); - expect(proposal.approvals).to.eq(0); + expect(proposal.approvals).to.eq(1); await expect(memberAccessPlugin.execute(id)) .to.be.revertedWithCustomError( memberAccessPlugin, @@ -1662,7 +1671,7 @@ describe("Member Access Plugin", function () { it("approves with the msg.sender address", async () => { expect((await memberAccessPlugin.getProposal(id)).approvals).to.equal( - 0, + 1, ); const tx = await memberAccessPlugin.connect(bob).approve( @@ -1675,19 +1684,24 @@ describe("Member Access Plugin", function () { expect(event!.args.editor).to.eq(bob.address); expect((await memberAccessPlugin.getProposal(id)).approvals).to.equal( - 1, + 2, ); }); }); - describe("canExecute:", async () => { - let id = 1; + describe("canExecute:", () => { + let id = 0; beforeEach(async () => { await proposeNewEditor(bob.address); // have 2 editors + await mineBlock(); + + expect(await memberAccessPlugin.isEditor(alice.address)).to.be.true; + expect(await memberAccessPlugin.isEditor(bob.address)).to.be.true; + expect(await memberAccessPlugin.isEditor(charlie.address)).to.be.false; // Alice approves await memberAccessPlugin.proposeNewMember(EMPTY_DATA, charlie.address); - id = 1; + id = 0; }); it("returns `false` if the proposal has not reached the minimum approval yet", async () => { @@ -1698,6 +1712,12 @@ describe("Member Access Plugin", function () { }); it("returns `false` if the proposal is already executed", async () => { + expect((await memberAccessPlugin.getProposal(id)).executed).to.be.false; + expect((await memberAccessPlugin.getProposal(id)).actions.length).to.eq( + 1, + ); + + // Approve and execute await memberAccessPlugin.connect(bob).approve(id, false); expect((await memberAccessPlugin.getProposal(id)).executed).to.be.true; @@ -1706,14 +1726,15 @@ describe("Member Access Plugin", function () { }); }); - describe("execute:", async () => { - let id = 1; + describe("execute:", () => { + let id = 0; beforeEach(async () => { await proposeNewEditor(bob.address); // have 2 editors + await mineBlock(); // Alice approves await memberAccessPlugin.proposeNewMember(EMPTY_DATA, charlie.address); - id = 1; + id = 0; }); it("reverts if the minimum approval is not met", async () => { @@ -1758,27 +1779,4 @@ describe("Member Access Plugin", function () { true, // auto execute ).then((tx) => tx.wait()); }; - - const proposeRemoveEditor = (_editor: string, proposer = alice) => { - const actions: IDAO.ActionStruct[] = [ - { - to: mainVotingPlugin.address, - value: 0, - data: MainVotingPlugin__factory.createInterface().encodeFunctionData( - "removeAddresses", - [[_editor]], - ), - }, - ]; - - return mainVotingPlugin.connect(proposer).createProposal( - toUtf8Bytes("ipfs://"), - actions, - 0, // fail safe - 0, // start date - 0, // end date - VoteOption.Yes, - true, // auto execute - ).then((tx) => tx.wait()); - }; });