├── .husky ├── .gitignore ├── pre-commit └── commit-msg ├── tasks ├── deploy │ ├── index.ts │ └── dao.ts ├── accounts.ts ├── clean.ts └── utils.ts ├── .gitattributes ├── .czrc ├── .commitlintrc.yaml ├── .solhintignore ├── .github └── FUNDING.yaml ├── .lintstagedrc ├── .env.example ├── .yarnrc.yml ├── .prettierrc.yaml ├── .editorconfig ├── .eslintignore ├── .prettierignore ├── .gitignore ├── test ├── types.ts ├── token │ └── Token.ts └── governor │ └── Governor.ts ├── .solcover.js ├── .solhint.json ├── .eslintrc.yaml ├── tsconfig.json ├── contracts ├── MyNftToken.sol ├── MyGovernor.sol └── Timelock.sol ├── hardhat.config.ts ├── README.md └── package.json /.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /tasks/deploy/index.ts: -------------------------------------------------------------------------------- 1 | import "./dao"; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.sol linguist-language=Solidity -------------------------------------------------------------------------------- /.czrc: -------------------------------------------------------------------------------- 1 | { 2 | "path": "cz-conventional-changelog" 3 | } 4 | -------------------------------------------------------------------------------- /.commitlintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "@commitlint/config-conventional" 3 | -------------------------------------------------------------------------------- /.solhintignore: -------------------------------------------------------------------------------- 1 | # directories 2 | **/artifacts 3 | **/node_modules 4 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn dlx lint-staged 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yaml: -------------------------------------------------------------------------------- 1 | custom: ["https://gitcoin.co/grants/1657/paulrberg-open-source-engineering"] 2 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn dlx commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.lintstagedrc: -------------------------------------------------------------------------------- 1 | { 2 | "*.{js,json,md,sol,ts,yaml,yml}": [ 3 | "prettier --config ./.prettierrc.yaml --write" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | INFURA_API_KEY=zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz 2 | MNEMONIC=here is where your twelve words mnemonic should be put my friend 3 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 5 | spec: "@yarnpkg/plugin-interactive-tools" 6 | 7 | yarnPath: .yarn/releases/yarn-3.0.2.cjs 8 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | arrowParens: avoid 2 | bracketSpacing: true 3 | endOfLine: auto 4 | printWidth: 120 5 | singleQuote: false 6 | tabWidth: 2 7 | trailingComma: all 8 | 9 | overrides: 10 | - files: "*.sol" 11 | options: 12 | tabWidth: 4 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # All files 7 | [*] 8 | charset = utf-8 9 | end_of_line = lf 10 | indent_size = 2 11 | indent_style = space 12 | insert_final_newline = true 13 | trim_trailing_whitespace = true 14 | 15 | [*.sol] 16 | indent_size = 4 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/ 3 | **/.coverage_artifacts 4 | **/.coverage_cache 5 | **/.coverage_contracts 6 | **/artifacts 7 | **/build 8 | **/cache 9 | **/coverage 10 | **/dist 11 | **/node_modules 12 | **/typechain 13 | 14 | # files 15 | *.env 16 | *.log 17 | .pnp.* 18 | coverage.json 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/ 3 | **/.coverage_artifacts 4 | **/.coverage_cache 5 | **/.coverage_contracts 6 | **/artifacts 7 | **/build 8 | **/cache 9 | **/coverage 10 | **/dist 11 | **/node_modules 12 | **/typechain 13 | 14 | # files 15 | *.env 16 | *.log 17 | .pnp.* 18 | coverage.json 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /tasks/accounts.ts: -------------------------------------------------------------------------------- 1 | import { Signer } from "@ethersproject/abstract-signer"; 2 | import { task } from "hardhat/config"; 3 | 4 | task("accounts", "Prints the list of accounts", async (_taskArgs, hre) => { 5 | const accounts: Signer[] = await hre.ethers.getSigners(); 6 | 7 | for (const account of accounts) { 8 | console.log(await account.getAddress()); 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /tasks/clean.ts: -------------------------------------------------------------------------------- 1 | import fsExtra from "fs-extra"; 2 | import { TASK_CLEAN } from "hardhat/builtin-tasks/task-names"; 3 | import { task } from "hardhat/config"; 4 | 5 | task(TASK_CLEAN, "Overrides the standard clean task", async function (_taskArgs, _hre, runSuper) { 6 | await fsExtra.remove("./coverage"); 7 | await fsExtra.remove("./coverage.json"); 8 | await runSuper(); 9 | }); 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # directories 2 | .yarn/* 3 | !.yarn/patches 4 | !.yarn/releases 5 | !.yarn/plugins 6 | !.yarn/sdks 7 | !.yarn/versions 8 | **/artifacts 9 | **/build 10 | **/cache 11 | **/coverage 12 | **/.coverage_artifacts 13 | **/.coverage_cache 14 | **/.coverage_contracts 15 | **/dist 16 | **/node_modules 17 | **/typechain 18 | 19 | # files 20 | *.env 21 | *.log 22 | .pnp.* 23 | coverage.json 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | -------------------------------------------------------------------------------- /test/types.ts: -------------------------------------------------------------------------------- 1 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 2 | import { Fixture } from "ethereum-waffle"; 3 | 4 | declare module "mocha" { 5 | export interface Context { 6 | loadFixture: (fixture: Fixture) => Promise; 7 | signers: Signers; 8 | } 9 | } 10 | 11 | export interface Signers { 12 | admin: SignerWithAddress; 13 | tokenReceiver: SignerWithAddress; 14 | delegatee: SignerWithAddress; 15 | } 16 | -------------------------------------------------------------------------------- /tasks/utils.ts: -------------------------------------------------------------------------------- 1 | import { ethers } from "ethers"; 2 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 3 | 4 | export const getExpectedContractAddress = async (deployer: SignerWithAddress): Promise => { 5 | const adminAddressTransactionCount = await deployer.getTransactionCount(); 6 | const expectedContractAddress = ethers.utils.getContractAddress({ 7 | from: deployer.address, 8 | nonce: adminAddressTransactionCount + 2, 9 | }); 10 | 11 | return expectedContractAddress; 12 | }; 13 | -------------------------------------------------------------------------------- /.solcover.js: -------------------------------------------------------------------------------- 1 | const shell = require("shelljs"); 2 | 3 | module.exports = { 4 | istanbulReporter: ["html", "lcov"], 5 | onCompileComplete: async function (_config) { 6 | await run("typechain"); 7 | }, 8 | onIstanbulComplete: async function (_config) { 9 | // We need to do this because solcover generates bespoke artifacts. 10 | shell.rm("-rf", "./artifacts"); 11 | shell.rm("-rf", "./typechain"); 12 | }, 13 | providerOptions: { 14 | mnemonic: process.env.MNEMONIC, 15 | }, 16 | skipFiles: ["mocks", "test"], 17 | }; 18 | -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "plugins": ["prettier"], 4 | "rules": { 5 | "code-complexity": ["error", 8], 6 | "compiler-version": ["error", ">=0.8.4"], 7 | "const-name-snakecase": "off", 8 | "constructor-syntax": "error", 9 | "func-visibility": ["error", { "ignoreConstructors": true }], 10 | "max-line-length": ["error", 120], 11 | "not-rely-on-time": "off", 12 | "prettier/prettier": [ 13 | "error", 14 | { 15 | "endOfLine": "auto" 16 | } 17 | ], 18 | "reason-string": ["warn", { "maxLength": 64 }] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | extends: 2 | - "eslint:recommended" 3 | - "plugin:@typescript-eslint/eslint-recommended" 4 | - "plugin:@typescript-eslint/recommended" 5 | - "prettier" 6 | parser: "@typescript-eslint/parser" 7 | parserOptions: 8 | project: "tsconfig.json" 9 | plugins: 10 | - "@typescript-eslint" 11 | root: true 12 | rules: 13 | "@typescript-eslint/no-floating-promises": 14 | - error 15 | - ignoreIIFE: true 16 | ignoreVoid: true 17 | "@typescript-eslint/no-inferrable-types": "off" 18 | "@typescript-eslint/no-unused-vars": 19 | - error 20 | - argsIgnorePattern: "_" 21 | varsIgnorePattern: "_" 22 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationMap": false, 5 | "emitDecoratorMetadata": true, 6 | "esModuleInterop": true, 7 | "experimentalDecorators": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "lib": ["es6"], 10 | "module": "commonjs", 11 | "moduleResolution": "node", 12 | "noImplicitAny": true, 13 | "removeComments": true, 14 | "resolveJsonModule": true, 15 | "sourceMap": false, 16 | "strict": true, 17 | "target": "es6" 18 | }, 19 | "exclude": ["node_modules"], 20 | "files": ["./hardhat.config.ts"], 21 | "include": ["tasks/**/*.ts", "test/**/*.ts", "typechain/**/*.d.ts", "typechain/**/*.ts"] 22 | } 23 | -------------------------------------------------------------------------------- /contracts/MyNftToken.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.6; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | import "@openzeppelin/contracts/utils/cryptography/draft-EIP712.sol"; 7 | import "@openzeppelin/contracts/token/ERC721/extensions/draft-ERC721Votes.sol"; 8 | import "@openzeppelin/contracts/utils/Counters.sol"; 9 | 10 | contract MyNftToken is ERC721, Ownable, EIP712, ERC721Votes { 11 | using Counters for Counters.Counter; 12 | 13 | Counters.Counter private _tokenIdCounter; 14 | 15 | constructor() ERC721("MyNftToken", "MTK") EIP712("MyNftToken", "1") {} 16 | 17 | function safeMint(address to) public onlyOwner { 18 | uint256 tokenId = _tokenIdCounter.current(); 19 | _tokenIdCounter.increment(); 20 | _safeMint(to, tokenId); 21 | } 22 | 23 | // The following functions are overrides required by Solidity. 24 | 25 | function _afterTokenTransfer( 26 | address from, 27 | address to, 28 | uint256 tokenId 29 | ) internal override(ERC721, ERC721Votes) { 30 | super._afterTokenTransfer(from, to, tokenId); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/token/Token.ts: -------------------------------------------------------------------------------- 1 | import hre from "hardhat"; 2 | import { Artifact } from "hardhat/types"; 3 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 4 | 5 | import { MyNftToken } from "../../typechain/MyNftToken"; 6 | import { Signers } from "../types"; 7 | import { expect } from "chai"; 8 | 9 | const { deployContract } = hre.waffle; 10 | 11 | describe("Unit tests", function () { 12 | before(async function () { 13 | this.signers = {} as Signers; 14 | 15 | const signers: SignerWithAddress[] = await hre.ethers.getSigners(); 16 | this.signers.admin = signers[0]; 17 | this.signers.tokenReceiver = signers[1]; 18 | this.signers.delegatee = signers[2]; 19 | }); 20 | 21 | describe("Token", function () { 22 | beforeEach(async function () { 23 | const tokenArtifact: Artifact = await hre.artifacts.readArtifact("MyNftToken"); 24 | this.token = await deployContract(this.signers.admin, tokenArtifact, []); 25 | }); 26 | 27 | it("owner should be able to mint new tokens", async function () { 28 | await this.token.connect(this.signers.admin).safeMint(this.signers.tokenReceiver.address); 29 | expect(await this.token.connect(this.signers.admin).ownerOf(0)).to.equal(this.signers.tokenReceiver.address); 30 | }); 31 | 32 | it("token holder can check vote to delegate balance", async function () { 33 | await this.token.connect(this.signers.admin).safeMint(this.signers.tokenReceiver.address); 34 | await this.token.connect(this.signers.tokenReceiver).delegate(this.signers.tokenReceiver.address); 35 | expect( 36 | await this.token.connect(this.signers.tokenReceiver).getVotes(this.signers.tokenReceiver.address), 37 | ).to.equal(1); 38 | }); 39 | 40 | it("token holder can delegate votes", async function () { 41 | await this.token.connect(this.signers.admin).safeMint(this.signers.tokenReceiver.address); 42 | await this.token.connect(this.signers.tokenReceiver).delegate(this.signers.delegatee.address); 43 | expect(await this.token.connect(this.signers.tokenReceiver).getVotes(this.signers.delegatee.address)).to.equal(1); 44 | }); 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /tasks/deploy/dao.ts: -------------------------------------------------------------------------------- 1 | import { task } from "hardhat/config"; 2 | import { getExpectedContractAddress } from "../utils"; 3 | 4 | import { 5 | MyNftToken, 6 | MyNftToken__factory, 7 | MyGovernor, 8 | MyGovernor__factory, 9 | Timelock, 10 | Timelock__factory, 11 | } from "../../typechain"; 12 | 13 | task("deploy:Dao").setAction(async function (_, { ethers, run }) { 14 | const timelockDelay = 2; 15 | 16 | const tokenFactory: MyNftToken__factory = await ethers.getContractFactory("MyNftToken"); 17 | 18 | const signerAddress = await tokenFactory.signer.getAddress(); 19 | const signer = await ethers.getSigner(signerAddress); 20 | 21 | const governorExpectedAddress = await getExpectedContractAddress(signer); 22 | 23 | const token: MyNftToken = await tokenFactory.deploy(); 24 | await token.deployed(); 25 | 26 | const timelockFactory: Timelock__factory = await ethers.getContractFactory("Timelock"); 27 | const timelock: Timelock = await timelockFactory.deploy(governorExpectedAddress, timelockDelay); 28 | await timelock.deployed(); 29 | 30 | const governorFactory: MyGovernor__factory = await ethers.getContractFactory("MyGovernor"); 31 | const governor: MyGovernor = await governorFactory.deploy(token.address, timelock.address); 32 | await governor.deployed(); 33 | 34 | console.log("Dao deployed to: ", { 35 | governorExpectedAddress, 36 | governor: governor.address, 37 | timelock: timelock.address, 38 | token: token.address, 39 | }); 40 | 41 | // We'll mint enough NFTs to be able to pass a proposal! 42 | await token.safeMint(signerAddress); 43 | await token.safeMint(signerAddress); 44 | await token.safeMint(signerAddress); 45 | await token.safeMint(signerAddress); 46 | 47 | console.log("Minted 4 NFTs to get us started"); 48 | 49 | // Transfer ownership to the timelock to allow it to perform actions on the NFT contract as part of proposal execution 50 | await token.transferOwnership(timelock.address); 51 | 52 | console.log("Granted the timelock ownership of the NFT Token"); 53 | 54 | await run("verify:verify", { 55 | address: token.address, 56 | }); 57 | 58 | await run("verify:verify", { 59 | address: timelock.address, 60 | constructorArguments: [governor.address, timelockDelay], 61 | }); 62 | 63 | await run("verify:verify", { 64 | address: governor.address, 65 | constructorArguments: [token.address, timelock.address], 66 | }); 67 | }); 68 | -------------------------------------------------------------------------------- /hardhat.config.ts: -------------------------------------------------------------------------------- 1 | import "@nomiclabs/hardhat-waffle"; 2 | import "@nomiclabs/hardhat-etherscan"; 3 | import "@typechain/hardhat"; 4 | import "hardhat-gas-reporter"; 5 | import "solidity-coverage"; 6 | 7 | import "./tasks/accounts"; 8 | import "./tasks/clean"; 9 | import "./tasks/deploy"; 10 | 11 | import { resolve } from "path"; 12 | 13 | import { config as dotenvConfig } from "dotenv"; 14 | import { HardhatUserConfig } from "hardhat/config"; 15 | import { NetworkUserConfig } from "hardhat/types"; 16 | 17 | dotenvConfig({ path: resolve(__dirname, "./.env") }); 18 | 19 | const chainIds = { 20 | goerli: 5, 21 | hardhat: 31337, 22 | kovan: 42, 23 | mainnet: 1, 24 | rinkeby: 4, 25 | ropsten: 3, 26 | }; 27 | 28 | // Ensure that we have all the environment variables we need. 29 | const mnemonic: string | undefined = process.env.MNEMONIC; 30 | if (!mnemonic) { 31 | throw new Error("Please set your MNEMONIC in a .env file"); 32 | } 33 | 34 | const infuraApiKey: string | undefined = process.env.INFURA_API_KEY; 35 | if (!infuraApiKey) { 36 | throw new Error("Please set your INFURA_API_KEY in a .env file"); 37 | } 38 | 39 | function getChainConfig(network: keyof typeof chainIds): NetworkUserConfig { 40 | const url: string = "https://" + network + ".infura.io/v3/" + infuraApiKey; 41 | return { 42 | accounts: { 43 | count: 10, 44 | mnemonic, 45 | path: "m/44'/60'/0'/0", 46 | }, 47 | chainId: chainIds[network], 48 | url, 49 | }; 50 | } 51 | 52 | const config: HardhatUserConfig = { 53 | defaultNetwork: "hardhat", 54 | gasReporter: { 55 | currency: "USD", 56 | enabled: process.env.REPORT_GAS ? true : false, 57 | excludeContracts: [], 58 | src: "./contracts", 59 | }, 60 | networks: { 61 | hardhat: { 62 | accounts: { 63 | mnemonic, 64 | }, 65 | chainId: chainIds.hardhat, 66 | }, 67 | goerli: getChainConfig("goerli"), 68 | kovan: getChainConfig("kovan"), 69 | rinkeby: getChainConfig("rinkeby"), 70 | ropsten: getChainConfig("ropsten"), 71 | }, 72 | etherscan: { apiKey: process.env.ETH_SCAN_API_KEY }, 73 | paths: { 74 | artifacts: "./artifacts", 75 | cache: "./cache", 76 | sources: "./contracts", 77 | tests: "./test", 78 | }, 79 | solidity: { 80 | version: "0.8.6", 81 | settings: { 82 | metadata: { 83 | // Not including the metadata hash 84 | // https://github.com/paulrberg/solidity-template/issues/31 85 | bytecodeHash: "none", 86 | }, 87 | // Disable the optimizer when debugging 88 | // https://hardhat.org/hardhat-network/#solidity-optimizer-support 89 | optimizer: { 90 | enabled: true, 91 | runs: 800, 92 | }, 93 | }, 94 | }, 95 | typechain: { 96 | outDir: "typechain", 97 | target: "ethers-v5", 98 | }, 99 | }; 100 | 101 | export default config; 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Solidity Template 2 | 3 | My favourite setup for writing Solidity smart contracts. 4 | 5 | - [Hardhat](https://github.com/nomiclabs/hardhat): compile and run the smart contracts on a local development network 6 | - [TypeChain](https://github.com/ethereum-ts/TypeChain): generate TypeScript types for smart contracts 7 | - [Ethers](https://github.com/ethers-io/ethers.js/): renowned Ethereum library and wallet implementation 8 | - [Waffle](https://github.com/EthWorks/Waffle): tooling for writing comprehensive smart contract tests 9 | - [Solhint](https://github.com/protofire/solhint): linter 10 | - [Solcover](https://github.com/sc-forks/solidity-coverage): code coverage 11 | - [Prettier Plugin Solidity](https://github.com/prettier-solidity/prettier-plugin-solidity): code formatter 12 | 13 | This is a GitHub template, which means you can reuse it as many times as you want. You can do that by clicking the "Use this 14 | template" button at the top of the page. 15 | 16 | ## Usage 17 | 18 | ### Pre Requisites 19 | 20 | Before running any command, you need to create a `.env` file and set a BIP-39 compatible mnemonic as an environment 21 | variable. Follow the example in `.env.example`. If you don't already have a mnemonic, use this [website](https://iancoleman.io/bip39/) to generate one. 22 | 23 | Then, proceed with installing dependencies: 24 | 25 | ```sh 26 | yarn install 27 | ``` 28 | 29 | ### Compile 30 | 31 | Compile the smart contracts with Hardhat: 32 | 33 | ```sh 34 | $ yarn compile 35 | ``` 36 | 37 | ### TypeChain 38 | 39 | Compile the smart contracts and generate TypeChain artifacts: 40 | 41 | ```sh 42 | $ yarn typechain 43 | ``` 44 | 45 | ### Lint Solidity 46 | 47 | Lint the Solidity code: 48 | 49 | ```sh 50 | $ yarn lint:sol 51 | ``` 52 | 53 | ### Lint TypeScript 54 | 55 | Lint the TypeScript code: 56 | 57 | ```sh 58 | $ yarn lint:ts 59 | ``` 60 | 61 | ### Test 62 | 63 | Run the Mocha tests: 64 | 65 | ```sh 66 | $ yarn test 67 | ``` 68 | 69 | ### Coverage 70 | 71 | Generate the code coverage report: 72 | 73 | ```sh 74 | $ yarn coverage 75 | ``` 76 | 77 | ### Report Gas 78 | 79 | See the gas usage per unit test and average gas per method call: 80 | 81 | ```sh 82 | $ REPORT_GAS=true yarn test 83 | ``` 84 | 85 | ### Clean 86 | 87 | Delete the smart contract artifacts, the coverage reports and the Hardhat cache: 88 | 89 | ```sh 90 | $ yarn clean 91 | ``` 92 | 93 | ### Deploy 94 | 95 | Deploy the contracts to Hardhat Network: 96 | 97 | ```sh 98 | $ yarn deploy --greeting "Bonjour, le monde!" 99 | ``` 100 | 101 | ## Syntax Highlighting 102 | 103 | If you use VSCode, you can enjoy syntax highlighting for your Solidity code via the 104 | [vscode-solidity](https://github.com/juanfranblanco/vscode-solidity) extension. The recommended approach to set the 105 | compiler version is to add the following fields to your VSCode user settings: 106 | 107 | ```json 108 | { 109 | "solidity.compileUsingRemoteVersion": "v0.8.4+commit.c7e474f2", 110 | "solidity.defaultCompiler": "remote" 111 | } 112 | ``` 113 | 114 | Where of course `v0.8.4+commit.c7e474f2` can be replaced with any other version. 115 | -------------------------------------------------------------------------------- /contracts/MyGovernor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.6; 3 | 4 | import "@openzeppelin/contracts/governance/Governor.sol"; 5 | import "@openzeppelin/contracts/governance/extensions/GovernorSettings.sol"; 6 | import "@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol"; 7 | import "@openzeppelin/contracts/governance/extensions/GovernorVotes.sol"; 8 | import "@openzeppelin/contracts/governance/extensions/GovernorTimelockCompound.sol"; 9 | 10 | contract MyGovernor is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorTimelockCompound { 11 | constructor(IVotes _token, ICompoundTimelock _timelock) 12 | Governor("MyGovernor") 13 | GovernorSettings( 14 | 1, /* 1 block */ 15 | 9, 16 | 2 17 | ) 18 | GovernorVotes(_token) 19 | GovernorTimelockCompound(_timelock) 20 | {} 21 | 22 | function quorum(uint256 blockNumber) public pure override returns (uint256) { 23 | return 3; 24 | } 25 | 26 | // The following functions are overrides required by Solidity. 27 | 28 | function votingDelay() public view override(IGovernor, GovernorSettings) returns (uint256) { 29 | return super.votingDelay(); 30 | } 31 | 32 | function votingPeriod() public view override(IGovernor, GovernorSettings) returns (uint256) { 33 | return super.votingPeriod(); 34 | } 35 | 36 | function state(uint256 proposalId) 37 | public 38 | view 39 | override(Governor, GovernorTimelockCompound) 40 | returns (ProposalState) 41 | { 42 | return super.state(proposalId); 43 | } 44 | 45 | function propose( 46 | address[] memory targets, 47 | uint256[] memory values, 48 | bytes[] memory calldatas, 49 | string memory description 50 | ) public override(Governor, IGovernor) returns (uint256) { 51 | return super.propose(targets, values, calldatas, description); 52 | } 53 | 54 | function proposalThreshold() public view override(Governor, GovernorSettings) returns (uint256) { 55 | return super.proposalThreshold(); 56 | } 57 | 58 | function _execute( 59 | uint256 proposalId, 60 | address[] memory targets, 61 | uint256[] memory values, 62 | bytes[] memory calldatas, 63 | bytes32 descriptionHash 64 | ) internal override(Governor, GovernorTimelockCompound) { 65 | super._execute(proposalId, targets, values, calldatas, descriptionHash); 66 | } 67 | 68 | function _cancel( 69 | address[] memory targets, 70 | uint256[] memory values, 71 | bytes[] memory calldatas, 72 | bytes32 descriptionHash 73 | ) internal override(Governor, GovernorTimelockCompound) returns (uint256) { 74 | return super._cancel(targets, values, calldatas, descriptionHash); 75 | } 76 | 77 | function _executor() internal view override(Governor, GovernorTimelockCompound) returns (address) { 78 | return super._executor(); 79 | } 80 | 81 | function supportsInterface(bytes4 interfaceId) 82 | public 83 | view 84 | override(Governor, GovernorTimelockCompound) 85 | returns (bool) 86 | { 87 | return super.supportsInterface(interfaceId); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@paulrberg/solidity-template", 3 | "description": "Setup for writing Solidity smart contracts", 4 | "version": "1.0.0", 5 | "author": { 6 | "name": "Paul Razvan Berg", 7 | "url": "https://paulrberg.com" 8 | }, 9 | "devDependencies": { 10 | "@codechecks/client": "^0.1.11", 11 | "@commitlint/cli": "^13.2.0", 12 | "@commitlint/config-conventional": "^13.2.0", 13 | "@ethersproject/abi": "^5.4.1", 14 | "@ethersproject/abstract-signer": "^5.4.1", 15 | "@ethersproject/bignumber": "^5.4.2", 16 | "@ethersproject/bytes": "^5.4.0", 17 | "@ethersproject/providers": "^5.4.5", 18 | "@nomiclabs/hardhat-ethers": "^2.0.2", 19 | "@nomiclabs/hardhat-etherscan": "^2.1.6", 20 | "@nomiclabs/hardhat-waffle": "^2.0.1", 21 | "@openzeppelin/test-helpers": "^0.5.15", 22 | "@typechain/ethers-v5": "^7.1.2", 23 | "@typechain/hardhat": "^2.3.0", 24 | "@types/chai": "^4.2.22", 25 | "@types/fs-extra": "^9.0.13", 26 | "@types/mocha": "^9.0.0", 27 | "@types/node": "^16.10.2", 28 | "@typescript-eslint/eslint-plugin": "^4.32.0", 29 | "@typescript-eslint/parser": "^4.32.0", 30 | "chai": "^4.3.4", 31 | "commitizen": "^4.2.4", 32 | "cross-env": "^7.0.3", 33 | "cz-conventional-changelog": "^3.3.0", 34 | "dotenv": "^10.0.0", 35 | "eslint": "^7.32.0", 36 | "eslint-config-prettier": "^8.3.0", 37 | "ethereum-waffle": "^3.4.0", 38 | "ethers": "^5.4.7", 39 | "fs-extra": "^10.0.0", 40 | "hardhat": "^2.6.4", 41 | "hardhat-gas-reporter": "^1.0.4", 42 | "husky": "^7.0.2", 43 | "lint-staged": "^11.2.0", 44 | "lodash": "^4.17.21", 45 | "mocha": "^9.1.2", 46 | "prettier": "^2.4.1", 47 | "prettier-plugin-solidity": "^1.0.0-beta.18", 48 | "shelljs": "^0.8.4", 49 | "solhint": "^3.3.6", 50 | "solhint-plugin-prettier": "^0.0.5", 51 | "solidity-coverage": "^0.7.17", 52 | "ts-generator": "^0.1.1", 53 | "ts-node": "^10.2.1", 54 | "typechain": "^5.1.2", 55 | "typescript": "^4.4.3" 56 | }, 57 | "files": [ 58 | "/contracts" 59 | ], 60 | "keywords": [ 61 | "blockchain", 62 | "ethereum", 63 | "hardhat", 64 | "smart-contracts", 65 | "solidity" 66 | ], 67 | "private": true, 68 | "scripts": { 69 | "clean": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat clean", 70 | "commit": "git-cz", 71 | "compile": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat compile", 72 | "coverage": "cross-env CODE_COVERAGE=true hardhat coverage --solcoverjs ./.solcover.js --temp artifacts --testfiles \"./test/**/*.ts\"", 73 | "deploy": "hardhat deploy:Dao", 74 | "lint": "yarn lint:sol && yarn lint:ts && yarn prettier:check", 75 | "lint:sol": "solhint --config ./.solhint.json --max-warnings 0 \"contracts/**/*.sol\"", 76 | "lint:ts": "eslint --config ./.eslintrc.yaml --ignore-path ./.eslintignore --ext .js,.ts .", 77 | "postinstall": "husky install", 78 | "prettier": "prettier --config ./.prettierrc.yaml --write \"**/*.{js,json,md,sol,ts}\"", 79 | "prettier:check": "prettier --check --config ./.prettierrc.yaml \"**/*.{js,json,md,sol,ts}\"", 80 | "test": "hardhat test", 81 | "typechain": "cross-env TS_NODE_TRANSPILE_ONLY=true hardhat typechain" 82 | }, 83 | "dependencies": { 84 | "@openzeppelin/contracts": "4.6.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /test/governor/Governor.ts: -------------------------------------------------------------------------------- 1 | import hre, { ethers } from "hardhat"; 2 | import { Artifact } from "hardhat/types"; 3 | import { SignerWithAddress } from "@nomiclabs/hardhat-ethers/dist/src/signer-with-address"; 4 | 5 | import { Governor } from "../../typechain/Governor"; 6 | import { MyNftToken } from "../../typechain/MyNftToken"; 7 | import { Timelock } from "../../typechain/Timelock"; 8 | import { Signers } from "../types"; 9 | import { expect } from "chai"; 10 | import { getExpectedContractAddress } from "../../tasks/utils"; 11 | 12 | const { deployContract } = hre.waffle; 13 | 14 | const timelockDelay = 2; 15 | 16 | describe("Unit tests", function () { 17 | before(async function () { 18 | this.signers = {} as Signers; 19 | 20 | const signers: SignerWithAddress[] = await hre.ethers.getSigners(); 21 | this.signers.admin = signers[0]; 22 | this.signers.tokenReceiver = signers[1]; 23 | this.signers.delegatee = signers[2]; 24 | 25 | const governorAddress = await getExpectedContractAddress(this.signers.admin); 26 | 27 | // deploy timelock 28 | const timelockArtifact: Artifact = await hre.artifacts.readArtifact("Timelock"); 29 | this.timelock = ( 30 | await deployContract(this.signers.admin, timelockArtifact, [governorAddress, timelockDelay]) 31 | ); 32 | 33 | // deploy token 34 | const tokenArtifact: Artifact = await hre.artifacts.readArtifact("MyNftToken"); 35 | this.token = await deployContract(this.signers.admin, tokenArtifact, []); 36 | 37 | // deploy governor 38 | const governorArtifact: Artifact = await hre.artifacts.readArtifact("MyGovernor"); 39 | this.governor = ( 40 | await deployContract(this.signers.admin, governorArtifact, [this.token.address, this.timelock.address]) 41 | ); 42 | 43 | await this.token.connect(this.signers.admin).safeMint(this.signers.admin.address); 44 | await this.token.connect(this.signers.admin).safeMint(this.signers.admin.address); 45 | await this.token.connect(this.signers.admin).safeMint(this.signers.admin.address); 46 | await this.token.connect(this.signers.admin).safeMint(this.signers.admin.address); 47 | await this.token.connect(this.signers.admin).delegate(this.signers.admin.address); 48 | 49 | await this.token.connect(this.signers.admin).safeMint(this.signers.tokenReceiver.address); 50 | await this.token.connect(this.signers.tokenReceiver).delegate(this.signers.tokenReceiver.address); 51 | }); 52 | 53 | describe("Governor", function () { 54 | it("propose and vote on a proposal", async function () { 55 | const calldata = new ethers.utils.AbiCoder().encode([], []); 56 | 57 | const txn = await this.governor 58 | .connect(this.signers.admin) 59 | .propose([this.token.address], [0], [calldata], "Send no ETH"); 60 | 61 | const receipt = await txn.wait(); 62 | const proposalId = receipt.events![0].args!.proposalId; 63 | 64 | // check proposal id exists 65 | expect(await this.governor.state(proposalId)).equal(0); 66 | 67 | await hre.network.provider.send("evm_mine"); 68 | 69 | await this.governor.connect(this.signers.admin).castVote(proposalId, 1); 70 | await this.governor.connect(this.signers.tokenReceiver).castVote(proposalId, 1); 71 | 72 | // check we have voted 73 | expect(await this.governor.state(proposalId)).to.eql(1); 74 | }); 75 | }); 76 | }); 77 | -------------------------------------------------------------------------------- /contracts/Timelock.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.8.6; 3 | 4 | import "@openzeppelin/contracts/utils/math/SafeMath.sol"; 5 | import "hardhat/console.sol"; 6 | 7 | contract Timelock { 8 | using SafeMath for uint256; 9 | 10 | event NewAdmin(address indexed newAdmin); 11 | event NewPendingAdmin(address indexed newPendingAdmin); 12 | event NewDelay(uint256 indexed newDelay); 13 | event CancelTransaction( 14 | bytes32 indexed txHash, 15 | address indexed target, 16 | uint256 value, 17 | string signature, 18 | bytes data, 19 | uint256 eta 20 | ); 21 | event ExecuteTransaction( 22 | bytes32 indexed txHash, 23 | address indexed target, 24 | uint256 value, 25 | string signature, 26 | bytes data, 27 | uint256 eta 28 | ); 29 | event QueueTransaction( 30 | bytes32 indexed txHash, 31 | address indexed target, 32 | uint256 value, 33 | string signature, 34 | bytes data, 35 | uint256 eta 36 | ); 37 | 38 | // NOTE: THESE VALUES ARE FOR TESTING ONLY! 39 | uint256 public constant GRACE_PERIOD = 2 days; 40 | uint256 public constant MINIMUM_DELAY = 1 seconds; 41 | uint256 public constant MAXIMUM_DELAY = 5 days; 42 | 43 | address public admin; 44 | address public pendingAdmin; 45 | uint256 public delay; 46 | 47 | mapping(bytes32 => bool) public queuedTransactions; 48 | 49 | constructor(address admin_, uint256 delay_) { 50 | require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); 51 | require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); 52 | 53 | admin = admin_; 54 | delay = delay_; 55 | } 56 | 57 | receive() external payable {} 58 | 59 | function setDelay(uint256 delay_) public { 60 | require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); 61 | require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); 62 | require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); 63 | delay = delay_; 64 | 65 | emit NewDelay(delay); 66 | } 67 | 68 | function acceptAdmin() public { 69 | require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); 70 | admin = msg.sender; 71 | pendingAdmin = address(0); 72 | 73 | emit NewAdmin(admin); 74 | } 75 | 76 | function setPendingAdmin(address pendingAdmin_) public { 77 | require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); 78 | pendingAdmin = pendingAdmin_; 79 | 80 | emit NewPendingAdmin(pendingAdmin); 81 | } 82 | 83 | function queueTransaction( 84 | address target, 85 | uint256 value, 86 | string memory signature, 87 | bytes memory data, 88 | uint256 eta 89 | ) public returns (bytes32) { 90 | require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); 91 | require( 92 | eta >= getBlockTimestamp().add(delay), 93 | "Timelock::queueTransaction: Estimated execution block must satisfy delay." 94 | ); 95 | 96 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 97 | queuedTransactions[txHash] = true; 98 | 99 | emit QueueTransaction(txHash, target, value, signature, data, eta); 100 | return txHash; 101 | } 102 | 103 | function cancelTransaction( 104 | address target, 105 | uint256 value, 106 | string memory signature, 107 | bytes memory data, 108 | uint256 eta 109 | ) public { 110 | require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); 111 | 112 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 113 | queuedTransactions[txHash] = false; 114 | 115 | emit CancelTransaction(txHash, target, value, signature, data, eta); 116 | } 117 | 118 | function executeTransaction( 119 | address target, 120 | uint256 value, 121 | string memory signature, 122 | bytes memory data, 123 | uint256 eta 124 | ) public payable returns (bytes memory) { 125 | require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); 126 | 127 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 128 | require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); 129 | require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); 130 | require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale."); 131 | 132 | queuedTransactions[txHash] = false; 133 | 134 | bytes memory callData; 135 | 136 | if (bytes(signature).length == 0) { 137 | callData = data; 138 | } else { 139 | callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); 140 | } 141 | 142 | // solium-disable-next-line security/no-call-value 143 | (bool success, bytes memory returnData) = target.call{ value: value }(callData); 144 | require(success, "Timelock::executeTransaction: Transaction execution reverted."); 145 | 146 | emit ExecuteTransaction(txHash, target, value, signature, data, eta); 147 | 148 | return returnData; 149 | } 150 | 151 | function getBlockTimestamp() internal view returns (uint256) { 152 | // solium-disable-next-line security/no-block-members 153 | return block.timestamp; 154 | } 155 | } 156 | --------------------------------------------------------------------------------