├── .gitignore ├── Readme.md ├── contracts ├── COMP.sol ├── GovernorAlpha.sol └── Timelock.sol ├── hardhat.config.js ├── package-lock.json ├── package.json ├── scripts └── Deploy.js ├── test └── sample-test.js └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | 3 | #Hardhat files 4 | cache 5 | artifacts 6 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## DEPRECATED 2 | 3 | Governor Alpha + Bravo is no longer recommended for new DAO deployments. Please use OpenZeppelin Governor for all new deployments. 4 | See: [OpenZeppelin's Contract Wizard](https://docs.openzeppelin.com/contracts/4.x/wizard) 5 | 6 | ## DEPRECATED 7 | 8 | TL:DR 9 | 10 | Want governance now? Clone the [github](https://github.com/withtally/Tutorial-Deploy-Governance) project, install the dependencies, add your Infura API and private key to the config and type `npx hardhat deploy` 11 | 12 | **Tally Governance Tutorial:** 13 | 14 | In this tutorial we show you how to deploy a Compound-style governance system that is secure and ready to be used in your DeFi project or protocol using HardHat. 15 | 16 | ## About [Tally](https://withtally.com/): 17 | 18 | [https://withtally.com/](https://withtally.com/) 19 | 20 | Tally is the premier place to interact with Governance on the Ethereum blockchain. We make tools to make governance easier, bring greater community involvement and enhanced transparency for decentralized protocols. 21 | 22 | If you would like to have your Compound-Style governance indexed by [Tally](http://www.withTally.com) indexers and added to the website, please [contact us](//dennison@withTally.com). 23 | 24 | **What we will accomplish:** 25 | 26 | 1. Review components of Compounds Governance system 27 | 2. Write a script that deploys Compounds Governance contracts to a network of your choice 28 | 3. Create a CLI task in HardHat that allows you to deploy secure governance's from the command line 29 | 30 | **Resources:** 31 | 32 | [Complete source code](https://github.com/withtally/Tutorial-Deploy-Governance) for this tutorial 33 | 34 | Check out recent votes and proposals on Compound with [Tally](http://www.withTally.com) 35 | 36 | [Tally Discord](https://discord.gg/Shx5Yjzqwm) 37 | 38 | [Compounds Documentation](https://compound.finance/docs) 39 | 40 | [HardHat](https://hardhat.org/getting-started/) 41 | 42 | **Why Compound?** 43 | 44 | Compound COMP governance system is one of the best in DeFi. It backs billions of dollars and its community regularly [proposes and votes](https://www.withtally.com/governance/compound) on changes via its governance. It is also the same governance tool used by [Uniswap](https://www.withtally.com/governance/uniswap) for governance and also backs billions of dollars. The system is comprised of three basic contracts, is easy to reason about, and has been very closely audited. 45 | 46 | ## Background 47 | 48 | ### The Compound Github 49 | 50 | All of the code required to launch your own governance can be found on the Compound Github. 51 | 52 | [https://github.com/compound-finance/compound-protocol/tree/master/contracts](https://github.com/compound-finance/compound-protocol/tree/master/contracts) 53 | 54 | The three contract we are interested in are: 55 | 56 | **[Comp](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/Comp.sol)** 57 | 58 | **[GovernorAlpha](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/GovernorAlpha.sol)** 59 | 60 | **[Timelock](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol)** 61 | 62 | ### **COMP** 63 | 64 | [https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/Comp.sol](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/Comp.sol) 65 | 66 | The COMP contract is what creates the COMP token. It is an ERC20 compatible token with support for checkpoints. Checkpointing is a system by which you can check the token balance of any user at any particular point in history. This is important because when a vote comes up that users need to vote on, you don't want individuals buying or selling tokens specifically to change the outcome of the vote and then dumping straight after a vote closes. To avoid this, checkpoints are used. By the time someone creates a proposal and puts it up for a vote in the Compound ecosystem, the voting power of all token holders is already known, and fixed, at a point in the past. This way users can still buy or sell tokens, but their balances won't affect their voting power. 67 | 68 | ### **GovernorAlpha** 69 | 70 | [https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/GovernorAlpha.sol](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Governance/GovernorAlpha.sol) 71 | 72 | The GovernorAlpha contract is the contract that does the actual "governance" part of the ecosystem. There are a number of hard-coded parameters that decide the functionality of governance, and the contract itself is the tool by which proposals are proposed, voted upon, and transferred to a timelock to be executed. The logic for secure voting is handled here. 73 | 74 | ### **Timelock** 75 | 76 | [https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol) 77 | 78 | The final component of the system is a Timelock. Timelock contracts essentially "delay" the execution of transactions to give the community a chance for a "sanity check" to be run over the outcome of a vote. It's important if a last minute bug is found in the system and it needs to be caught before a transaction is implemented. 79 | 80 | All three of these components work together with their own sphere of influence. The COMP token essentially functions as a voter registration tool (and as a tradable ERC20 token), the GovernorAlpha acts as a polling location- the place where voting happens, and the Timelock acts as a loading bay that holds decision for a set amount of time before executing them on the network. 81 | 82 | ### Get the Source Code 83 | 84 | First we need to get the COMP source code. There are two options for this to be sure you're getting the audited and deployed Code: The [Compound Github](https://github.com/compound-finance), or Etherscan via the [Compound Finance Doc's](https://compound.finance/docs). 85 | 86 | **Etherscan:** 87 | 88 | If you go the etherscan route, to get the addresses of the deployed Compound contracts visit [https://compound.finance/docs](https://compound.finance/docs). 89 | 90 | Since all Compound Finance code has been verified on Etherscan (don't use any code that isn't verified for a real project!) we can flip over to the contract information to see the source code and copy it. 91 | 92 | **For reference:** 93 | 94 | COMP: [https://etherscan.io/address/0xc00e94cb662c3520282e6f5717214004a7f26888](https://etherscan.io/address/0xc00e94cb662c3520282e6f5717214004a7f26888) 95 | 96 | Governance: [https://etherscan.io/address/0xc0da01a04c3f3e0be433606045bb7017a7323e38](https://etherscan.io/address/0xc0da01a04c3f3e0be433606045bb7017a7323e38) 97 | 98 | Timelock: [https://etherscan.io/address/0x6d903f6003cca6255d85cca4d3b5e5146dc33925](https://etherscan.io/address/0x6d903f6003cca6255d85cca4d3b5e5146dc33925) 99 | 100 | ## Create your project 101 | 102 | **Requirements:** 103 | 104 | You will need to have `node` installed on your machine. We will be using [HardHat](https://hardhat.org/getting-started/) for our scripting and deployment needs. 105 | 106 | **Get Started:** 107 | 108 | First we are going to create a directory for our project: 109 | 110 | `mkdir tutorial-governance` 111 | 112 | then CD into it: 113 | 114 | `cd tutorial-governance` 115 | 116 | We will want to initialize our node project: 117 | 118 | `npm init -y` 119 | 120 | The "-y" will pre-populate our `.json` file. 121 | 122 | Now we want to setup our HardHat project: 123 | 124 | `npx hardhat` 125 | 126 | This will setup our initial HardHat Project with some defaults. 127 | 128 | - Create a sample Project 129 | - Accept the project root 130 | - Accept creating a .gitignore 131 | - Accept installing the sample project dependencies 132 | 133 | At this point you might want to spend a couple minutes inspecting the HardHat setup if this is your first time working with it. 134 | 135 | Check out this guide for more info: [https://hardhat.org/getting-started/](https://hardhat.org/getting-started/) 136 | 137 | ### Create the files 138 | 139 | We will be creating three files in our `/contracts` folder: `COMP.sol` `Timelock.sol` `GovernorAlpha.sol`. You can take these files directly from the Compound Github or copy from the verified code in Etherescan. 140 | 141 | There will be a leftover file from the HardHat install called `Greeter.sol` in the contracts directory. You can delete this. 142 | 143 | When you are finished your `/contracts` folder should have three contracts in it. 144 | 145 | ```markdown 146 | /Contracts 147 | COMP.sol 148 | Timelock.sol 149 | GovernorAlpha.sol 150 | ``` 151 | 152 | **CUSTOMIZE and COMPILE** 153 | 154 | The next step is to compile our contracts, but before we do that we need to configure our Solidity Compiler version. Compounds contracts were compiled with Solidity version: 0.5.16, which is significantly older than the default 0.7.3 which is found in the default HardHat configuration. You can update the Compound files be compatible with the latest compiler, but you will lose the implied security guarantees of the existing audited code if you do that. 155 | 156 | At the top level of your project find the file: 157 | 158 | `hardhat.config.js` 159 | 160 | At the bottom of the file you will find an exports that includes the Solidity compiler version: 161 | 162 | ```jsx 163 | /** 164 | * @type import('hardhat/config').HardhatUserConfig 165 | */ 166 | module.exports = { 167 | solidity: "0.7.3", 168 | }; 169 | ``` 170 | 171 | Here change the "0.7.3" to "0.5.16" so the exports looks like this: 172 | 173 | ```jsx 174 | /** 175 | * @type import('hardhat/config').HardhatUserConfig 176 | */ 177 | module.exports = { 178 | solidity: "0.5.16", 179 | }; 180 | ``` 181 | 182 | Now we are ready to compile and this is where HardHat makes the development process so simple. (Note, make sure you have deleted the `Greeter.sol` contract or else the mismatch in Solidity compile versions will prevent you from compiling). 183 | 184 | `npx hardhat compile` 185 | 186 | You should see this output: 187 | 188 | ```markdown 189 | ❯ npx hardhat compile 190 | Compiling 3 files with 0.5.16 191 | contracts/COMP.sol:6:1: Warning: Experimental features are turned on. Do not use experimental features on live deployments. 192 | pragma experimental ABIEncoderV2; 193 | ^-------------------------------^ 194 | 195 | Compilation finished successfully 196 | ``` 197 | 198 | Easy! 199 | 200 | **Customizing Parameters** 201 | 202 | A number of parameters in the Compound system are hard coded, these parameters you will most likely want to customize for your installation: 203 | 204 | ### **COMP** 205 | 206 | ```jsx 207 | contract Comp { 208 | /// @notice EIP-20 token name for this token 209 | string public constant name = "MyTokenName"; 210 | 211 | /// @notice EIP-20 token symbol for this token 212 | string public constant symbol = "MTN"; 213 | 214 | /// @notice EIP-20 token decimals for this token 215 | uint8 public constant decimals = 18; 216 | 217 | /// @notice Total number of tokens in circulation 218 | uint public constant totalSupply = 10000000e18; // 10 million myTokens 219 | ``` 220 | 221 | In the COMP contract the constants we might want to alter are right at the top of the file: 222 | 223 | `name` 224 | 225 | `symbol` 226 | 227 | `decimals` 228 | 229 | `totalSupply` 230 | 231 | These are standard ERC20 properties, the Name, and Symbol (for exchanges that list the token), the decimals, and total supply. In this case, you don't want to alter the `decimals`, the ecosystem has made `18` the standard, and using a different number can cause problems or incompatibilities with the rest of the ERC20 ecosystem. 232 | 233 | Feel free to change `name`, `symbol` and `totalSupply` to whatever you like and run `npx hardhat compile` once again. 234 | 235 | ### **GovernorAlpha.sol** 236 | 237 | The next step is to customize `GovernorAlpha` contract. At the top of the source code file: `/contracts/GovernorAlpha.sol` that you created earlier with the copy-paste code from etherscan you should see the following: 238 | 239 | ```jsx 240 | contract GovernorAlpha { 241 | /// @notice The name of this contract 242 | string public constant name = "Compound Governor Alpha"; 243 | 244 | /// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed 245 | function quorumVotes() public pure returns (uint) { return 400000e18; } // 400,000 = 4% of Comp 246 | 247 | /// @notice The number of votes required in order for a voter to become a proposer 248 | function proposalThreshold() public pure returns (uint) { return 100000e18; } // 100,000 = 1% of Comp 249 | 250 | /// @notice The maximum number of actions that can be included in a proposal 251 | function proposalMaxOperations() public pure returns (uint) { return 10; } // 10 actions 252 | 253 | /// @notice The delay before voting on a proposal may take place, once proposed 254 | function votingDelay() public pure returns (uint) { return 1; } // 1 block 255 | 256 | /// @notice The duration of voting on a proposal, in blocks 257 | function votingPeriod() public pure returns (uint) { return 17280; } // ~3 days in blocks (assuming 15s blocks) 258 | ``` 259 | 260 | Here the items that interest us for customization are: 261 | 262 | `name` 263 | 264 | `quorumVotes()` 265 | 266 | `proposalThreshold()` 267 | 268 | `proposalMaxOperations()` 269 | 270 | `votingDelay()` 271 | 272 | `votingPeriod()` 273 | 274 | **Name**: This self explanatory, change it to whatever you would like. 275 | 276 | **QuorumVotes (`quorumVotes()`)** 277 | 278 | The quorum is the number of **YES** votes required to ensure that a vote is valid. The idea behind this is that some minimum number of votes need to be cast in order for the vote to be seen as legitimate: it wouldn't make sense if in an ecosystem of 10 million possible votes a proposal passed 2 yes votes to 1 no vote. In the Compound ecosystem at least 400,000 COMP are required to vote yes for a proposal to pass. In todays money ($155 per comp) thats over $60 Million dollars worth of votes needed to get something to pass. Needless to say, if you customize this, you will want to pick a number you feel is reasonable for your system. 279 | 280 | **ProposalThreshold (`proposalThreshold()`)** 281 | 282 | To prevent a system where countless spam proposals are created, a proposal threshold requires an address has a certain number of votes before they can make a proposal. In the case of COMP, it's 100,000. Pick a number that works for you. 283 | 284 | **ProposalMaxOperations ( `proposalMaxOperations()`)** 285 | 286 | This is the maximum number of operations that can be executed in a single proposal. Unless you have a good reason, I would probably leave this alone. 287 | 288 | **VotingDelay (`votingDelay()`)** 289 | 290 | ********This is the length of time between which a proposal can be created and it is available to be voted upon. By requiring at least one block to pass, the governance is protected from Flash Loan attacks that might borrow a large number of tokens, propose a vote, and vote on it all in one block. Unless you have a good reason, I would leave this alone. 291 | 292 | **VotingPeriod ( `votingPeriod()`)** 293 | 294 | The length of time for which proposals are available to be voted upon, with time in Ethereum Blocks. Pick what you feel is reasonable for use case. 295 | 296 | ### **TimeLock.sol** 297 | 298 | Looking at your `Timelock.sol` source code you will see there is first a `SafeMath` library contract at the top of your file. The `Timelock` contract starts at line 171. There you will see the following: 299 | 300 | ```jsx 301 | contract Timelock { 302 | using SafeMath for unit; 303 | 304 | [...list of events omitted for clarity] 305 | 306 | uint public constant GRACE_PERIOD = 14 days; 307 | uint public constant MINIMUM_DELAY = 2 days; 308 | uint public constant MAXIMUM_DELAY = 30 days; 309 | ``` 310 | 311 | The constants available to modify are: 312 | 313 | `GRACE_PERIOD` 314 | 315 | `MINIMUM_DELAY` 316 | 317 | `MAXIMUM_DELAY` 318 | 319 | **GRACE_PERIOD -** Once a transaction has been loaded into a timelock for execution, it is required that someone still "press the button" to have it execute and pay the gas required. The `GRACE_PERIOD` is essentially how long between the time at which a transaction becomes available to execute, and when the proposal had intended the transaction to be executed (the `eta` component on a Proposal- see line 140 on `COMP.sol`). After the `GRACE_PERIOD` and `eta` combined expire, the transaction is considered stale (see line 255 `Timelock.sol`) and is not possible to execute. 320 | 321 | **MINIMUM_DELAY & MAXIMUM_DELAY -** When deploying the `Timelock.sol` contract, one of the constructor arguments is `delay` (see: line 195 `Timlock.sol`). The `MINIMUM_DELAY` & `MAXIMUM_DELAY` serve to set as hardcoded limits on how long the `Timelock` contract needs to wait before executing a transaction. 322 | 323 | In general, I would recommend leaving these set as they are, but again- if you need something different feel free to customize them. Once you're set, run `npx hardhat compile` again. 324 | 325 | ## **Deployment**: 326 | 327 | To deploy a contract using HardHat, we need to first write a script. When we first created our project HardHat created a sample script for us, but we're going to start from scratch. To deploy a COMP governance system we need to deploy our contracts in a specific order as some contracts need the address of the others in their constructor functions: 328 | 329 | First, we will deploy COMP, the token. For this our constructor only needs the address of where to send the initial (fixed) token supply. 330 | 331 | Create a `Deploy.js` file in your `/scripts` folder, copy/paste the following code: 332 | 333 | ```jsx 334 | const hre = require("hardhat"); 335 | const ethers = hre.ethers; 336 | 337 | async function main() { 338 | 339 | // Compile our Contracts, just in case 340 | await hre.run('compile'); 341 | 342 | // Get a signer from the HardHard environment 343 | // Learn about signers here: https://docs.ethers.io/v4/api-wallet.html 344 | const [tokenRecipient] = await ethers.getSigners(); 345 | 346 | // This gets the contract from 347 | const Token = await hre.ethers.getContractFactory("Comp"); 348 | const token = await Token.deploy(tokenRecipient.address); 349 | 350 | await token.deployed(); 351 | console.log(`Token deployed to: ${token.address}`); 352 | 353 | 354 | const initialBalance = await token.balanceOf(tokenRecipient.address); 355 | console.log(`${initialBalance / 1e18} tokens transfered to ${tokenRecipient.address}`); 356 | } 357 | 358 | main() 359 | .then(() => process.exit(0)) 360 | .catch(error => { 361 | console.error(error); 362 | process.exit(1); 363 | }); 364 | ``` 365 | 366 | To run it using the built in HardHard environment blockchain use: 367 | 368 | `npx hardhat run scripts/Deploy.js` 369 | 370 | You should see the following output (Your addresses and token balance might be different) 371 | 372 | ```markdown 373 | ❯ npx hardhat run scripts/Deploy.js 374 | Nothing to compile 375 | Token deployed to: 0x5FbDB2315678afecb367f032d93F642f64180aa3 376 | 10000000 tokens transfered to 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 377 | ``` 378 | 379 | Great! We've just deployed our COMP token to an ephemeral development blockchain that exists only inside this running program instance. (We will get to deploying to a real network later). 380 | 381 | **Timelock** 382 | 383 | Next we will deploy the Timelock because the governance needs to know the Timelock address. 384 | 385 | First, lets get another address to be the Timelock admin, change line: 386 | 387 | ```jsx 388 | const [tokenRecipient] = await ethers.getSigners(); 389 | ``` 390 | 391 | to get another signer 392 | 393 | ```jsx 394 | const [tokenRecipient, timelockAdmin] = await ethers.getSigners(); 395 | 396 | ``` 397 | 398 | Now that we have a signer to be our `Timelock` admin, we can choose a delay between our `MINIMUM_DELAY` and `MAXIMUM_DELAY` constants, and pass that in. 399 | 400 | Add to our file, after the line where we `await token.deployed()` the following: 401 | 402 | ```jsx 403 | // deploy timelock 404 | const delay = 5; 405 | const Timelock = await ethers.getContractFactory("Timelock"); 406 | const timelock = await Timelock.deploy(timelockAdmin.address, delay); 407 | await timelock.deployed(); 408 | await timelock.deployTransaction.wait(); 409 | // A nice sanity check to see the address 410 | console.log(`TimeLock deployed to: ${timelock.address}`); 411 | ``` 412 | 413 | Run `npx hardhat run scripts/Deploy.js` again. 414 | 415 | NOTE: If you get the following error: `Error: VM Exception while processing transaction: revert Timelock::constructor: Delay must exceed minimum delay.` This means your deployment of Timelock didn't go through because the delay you chose wasn't between your `MINIMUM_DELAY` and `MAXIMUM_DELAY`. The trick is though that in the code, these days are specified in **DAYS**, which Solidity estimates in SECONDS. The delay you send in to the constructor is denoted in seconds, so you have to choose a number of seconds that represents the time in days you want to use as a delay. So if you want **5** days instead of 5 seconds in the example above you would do: 5*60*60*24= **432,000**, and you would use that number for your delay instead. 416 | 417 | ****Run `npx hardhat run scripts/Deploy.js` again and it should work. 418 | 419 | **GovernorAlpha** 420 | 421 | Now that you've deployed your governance token, your timelock contract, it's time to deploy the GovernorAlpha contract. 422 | 423 | To deploy we will use the same pattern as above. First let's get another signer, a "Guardian", an address that has the power to cancel proposals (use this wisely!). 424 | 425 | ```jsx 426 | const [tokenRecipient, timelockAdmin, guardian] = await ethers.getSigners(); 427 | ``` 428 | 429 | Now add the following code after the Timelock gets deployed: 430 | 431 | ```jsx 432 | // Deploy Governance 433 | const Gov = await ethers.getContractFactory("GovernorAlpha"); 434 | const gov = await gov.deploy(timelock.address, token.address, guardian.address); 435 | await gov.deployed(); 436 | await gov.deployTransaction.wait(); 437 | 438 | ... 439 | console.log(`GovernorAlpha deployed to: ${gov.address}`) 440 | ``` 441 | 442 | The full code so far is: 443 | 444 | ```jsx 445 | const hre = require("hardhat"); 446 | const ethers = hre.ethers; 447 | 448 | async function main() { 449 | 450 | // Compile our Contracts, just in case 451 | await hre.run('compile'); 452 | 453 | // Get a signer from the HardHard environment 454 | // Learn about signers here: https://docs.ethers.io/v4/api-wallet.html 455 | const [tokenRecipient, timelockAdmin, guardian] = await ethers.getSigners(); 456 | 457 | // This gets the contract from 458 | const Token = await hre.ethers.getContractFactory("Comp"); 459 | const token = await Token.deploy(tokenRecipient.address); 460 | await token.deployed(); 461 | await token.deployTransaction.wait(); 462 | 463 | // Deploy Timelock 464 | const delay = 172800; 465 | const Timelock = await ethers.getContractFactory("Timelock"); 466 | const timelock = await Timelock.deploy(timelockAdmin.address, delay); 467 | await timelock.deployed(); 468 | await timelock.deployTransaction.wait(); 469 | 470 | // Deploy Governance 471 | const Gov = await ethers.getContractFactory("GovernorAlpha"); 472 | const gov = await Gov.deploy(timelock.address, token.address, guardian.address); 473 | await gov.deployed(); 474 | await gov.deployTransaction.wait(); 475 | 476 | console.log(`Token deployed to: ${token.address}`); 477 | console.log(`TimeLock deployed to: ${timelock.address}`); 478 | console.log(`GovernorAlpha deployed to: ${gov.address}`) 479 | 480 | const initialBalance = await token.balanceOf(tokenRecipient.address); 481 | console.log(`${initialBalance / 1e18} tokens transfered to ${tokenRecipient.address}`); 482 | } 483 | 484 | main() 485 | .then(() => process.exit(0)) 486 | .catch(error => { 487 | console.error(error); 488 | process.exit(1); 489 | }); 490 | ``` 491 | 492 | # Deploy to Network: 493 | 494 | Now that we have this working, deploying to a real network requires only two things: 495 | 496 | 1. Changes to the `hardhat.config.js` file to add a new network 497 | 2. Private keys to the addresses you with to use as Signers. 498 | 499 | **Update the `hardhat.config.js` file** 500 | 501 | To deploy to a network using HardHat we need to first give it some information about the network we want to deploy to. In our case, we are deploying to `Rinkeby` and we're going to use Infura as our network connection. (Sign up at [Infura](https://infura.io/) to get your own API key). 502 | 503 | We will also need some private keys, which unfortunately even in 2020 (going on 2021), is still the most cumbersome part of the deployment. 504 | 505 | Add a `networks` property to your `hardhat.config.js` file exports and as a sub-property add your network (in our case `Rinkeby`) along with an array of private keys. 506 | 507 | ```jsx 508 | module.exports = { 509 | networks: { 510 | rinkeby: { 511 | url: "https://rinkeby.infura.io/v3/<>", 512 | accounts: ["PrivateKey1", "PrivateKey2", "PrivateKey3"] 513 | } 514 | }, 515 | solidity: "0.5.16", 516 | }; 517 | ``` 518 | 519 | Now to deploy to your network you type: 520 | 521 | ```jsx 522 | npx hardhat run scripts/Deploy.js --network rinkeby 523 | ``` 524 | 525 | ## **Create a Task** 526 | 527 | Finally, we're using HardHat because it helps automate deployments. Let's create a task so we can deploy our governance directly from the command line. 528 | 529 | Inside your `hardhat.config.js` file, we will add the following task, below the already existing sample task (delete the sample task if you like) 530 | 531 | ```markdown 532 | task("Deploy", "Deploys a COMPound style governance system", async () => { 533 | ..... we will fill this out. 534 | 535 | }); 536 | ``` 537 | 538 | Lets reuse our code that we wrote in our script, the goal is to make the deployment script modular and export the function so we can import it in our task. 539 | 540 | ```markdown 541 | // Delete this as it will cause our task to run twice 542 | // main() 543 | // .then(() => process.exit(0)) 544 | // .catch(error => { 545 | // console.error(error); 546 | // process.exit(1); 547 | // }); 548 | 549 | // Add a module.exports 550 | module.exports = { 551 | deploy: main 552 | } 553 | ``` 554 | 555 | Additionally, we're going to want to pass in the address for the initial token holder, guardian, etc.., so that it's not based on `ethers.getSigners()` which comes from our config file (and requires private keys!) 556 | 557 | Lets pass in an object `{tokenRecipient, timeLockAdmin, guardian}` into our `main()` function so that it looks like this: 558 | 559 | ```markdown 560 | async function main({tokenRecipient, timeLockAdmin, guardian}) 561 | ``` 562 | 563 | Unlike the output from `getSigners()` these variables will not be signers- there won't be private keys attached, so there will be no `.address` property, instead their value is the address. Remove the `.address` from just these three variables when they are passed into the deployment functions. Your new code should look like this: 564 | 565 | ```jsx 566 | const hre = require("hardhat"); 567 | const ethers = hre.ethers; 568 | 569 | async function main({tokenRecipient, timeLockAdmin, guardian}) { 570 | 571 | // Compile our Contracts, just in case 572 | await hre.run('compile'); 573 | 574 | // This gets the contract from 575 | const Token = await hre.ethers.getContractFactory("Comp"); 576 | const token = await Token.deploy(tokenRecipient); 577 | await token.deployed(); 578 | await token.deployTransaction.wait(); 579 | 580 | // Deploy Timelock 581 | const delay = 172800; 582 | const Timelock = await ethers.getContractFactory("Timelock"); 583 | const timelock = await Timelock.deploy(timeLockAdmin, delay); 584 | await timelock.deployed(); 585 | await timelock.deployTransaction.wait(); 586 | 587 | // Deploy Governance 588 | const Gov = await ethers.getContractFactory("GovernorAlpha"); 589 | const gov = await Gov.deploy(timelock.address, token.address, guardian); 590 | await gov.deployed(); 591 | await gov.deployTransaction.wait(); 592 | 593 | console.log(`Token deployed to: ${token.address}`); 594 | console.log(`TimeLock deployed to: ${timelock.address}`); 595 | console.log(`GovernorAlpha deployed to: ${gov.address}`) 596 | 597 | const initialBalance = await token.balanceOf(tokenRecipient); 598 | console.log(`${initialBalance / 1e18} tokens transfered to ${tokenRecipient}`); 599 | } 600 | 601 | module.exports = { 602 | deploy: main 603 | } 604 | ``` 605 | 606 | Now we need to add to the HardHat task the ability to get `tokenRecipient`, `timeLockAdmin`, `guardian` from the command line and pass it to our `main` function. To do this, HardHard allows you to collect Params directly from the command line using the `.addParam()` function and access it inside the task. 607 | 608 | Update the task to look like this: 609 | 610 | ```jsx 611 | task("Deploy", "Deploys a COMPound style governance system") 612 | .addParam("token", "The address to receive the initial supply") 613 | .addParam("timelock", "The timelock administrator") 614 | .addParam("guardian", "The governor guardian").setAction(async taskArgs => { 615 | 616 | const { deploy } = require("./scripts/Deploy"); 617 | 618 | await deploy({ 619 | tokenRecipient: taskArgs.token, 620 | timeLockAdmin: taskArgs.timelock, 621 | guardian: taskArgs.guardian 622 | }); 623 | 624 | }) 625 | ``` 626 | 627 | Now to deploy you need only one private key in your `module.exports` in the `hardhat.config.js` file, this private key is used to deploy the contracts, but even if it's compromised, your governance system isn't in danger as they key addresses for the governance system are entered on the CLI. It also means you can deploy as many governance systems as you like, quickly and easily, from the CLI. 628 | 629 | To deploy: 630 | 631 | `npx hardhat Deploy --token 0xAddressToReceivetokens --timelock 0xAddressTimeLockAdmin --guardian 0xAddressGovernorAlphaAdmin --network rinkeby` 632 | 633 | You can see the final code here: 634 | 635 | [https://github.com/withtally/Tutorial-Deploy-Governance](https://github.com/withtally/Tutorial-Deploy-Governance) 636 | -------------------------------------------------------------------------------- /contracts/COMP.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2020-03-04 3 | */ 4 | 5 | pragma solidity ^0.5.16; 6 | pragma experimental ABIEncoderV2; 7 | 8 | contract Comp { 9 | /// @notice EIP-20 token name for this token 10 | string public constant name = "MyTutorialToken"; 11 | 12 | /// @notice EIP-20 token symbol for this token 13 | string public constant symbol = "MTT"; 14 | 15 | /// @notice EIP-20 token decimals for this token 16 | uint8 public constant decimals = 18; 17 | 18 | /// @notice Total number of tokens in circulation 19 | uint public constant totalSupply = 10000000e18; // 10 million Comp 20 | 21 | /// @notice Allowance amounts on behalf of others 22 | mapping (address => mapping (address => uint96)) internal allowances; 23 | 24 | /// @notice Official record of token balances for each account 25 | mapping (address => uint96) internal balances; 26 | 27 | /// @notice A record of each accounts delegate 28 | mapping (address => address) public delegates; 29 | 30 | /// @notice A checkpoint for marking number of votes from a given block 31 | struct Checkpoint { 32 | uint32 fromBlock; 33 | uint96 votes; 34 | } 35 | 36 | /// @notice A record of votes checkpoints for each account, by index 37 | mapping (address => mapping (uint32 => Checkpoint)) public checkpoints; 38 | 39 | /// @notice The number of checkpoints for each account 40 | mapping (address => uint32) public numCheckpoints; 41 | 42 | /// @notice The EIP-712 typehash for the contract's domain 43 | bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); 44 | 45 | /// @notice The EIP-712 typehash for the delegation struct used by the contract 46 | bytes32 public constant DELEGATION_TYPEHASH = keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)"); 47 | 48 | /// @notice A record of states for signing / validating signatures 49 | mapping (address => uint) public nonces; 50 | 51 | /// @notice An event thats emitted when an account changes its delegate 52 | event DelegateChanged(address indexed delegator, address indexed fromDelegate, address indexed toDelegate); 53 | 54 | /// @notice An event thats emitted when a delegate account's vote balance changes 55 | event DelegateVotesChanged(address indexed delegate, uint previousBalance, uint newBalance); 56 | 57 | /// @notice The standard EIP-20 transfer event 58 | event Transfer(address indexed from, address indexed to, uint256 amount); 59 | 60 | /// @notice The standard EIP-20 approval event 61 | event Approval(address indexed owner, address indexed spender, uint256 amount); 62 | 63 | /** 64 | * @notice Construct a new Comp token 65 | * @param account The initial account to grant all the tokens 66 | */ 67 | constructor(address account) public { 68 | balances[account] = uint96(totalSupply); 69 | emit Transfer(address(0), account, totalSupply); 70 | } 71 | 72 | /** 73 | * @notice Get the number of tokens `spender` is approved to spend on behalf of `account` 74 | * @param account The address of the account holding the funds 75 | * @param spender The address of the account spending the funds 76 | * @return The number of tokens approved 77 | */ 78 | function allowance(address account, address spender) external view returns (uint) { 79 | return allowances[account][spender]; 80 | } 81 | 82 | /** 83 | * @notice Approve `spender` to transfer up to `amount` from `src` 84 | * @dev This will overwrite the approval amount for `spender` 85 | * and is subject to issues noted [here](https://eips.ethereum.org/EIPS/eip-20#approve) 86 | * @param spender The address of the account which may transfer tokens 87 | * @param rawAmount The number of tokens that are approved (2^256-1 means infinite) 88 | * @return Whether or not the approval succeeded 89 | */ 90 | function approve(address spender, uint rawAmount) external returns (bool) { 91 | uint96 amount; 92 | if (rawAmount == uint(-1)) { 93 | amount = uint96(-1); 94 | } else { 95 | amount = safe96(rawAmount, "Comp::approve: amount exceeds 96 bits"); 96 | } 97 | 98 | allowances[msg.sender][spender] = amount; 99 | 100 | emit Approval(msg.sender, spender, amount); 101 | return true; 102 | } 103 | 104 | /** 105 | * @notice Get the number of tokens held by the `account` 106 | * @param account The address of the account to get the balance of 107 | * @return The number of tokens held 108 | */ 109 | function balanceOf(address account) external view returns (uint) { 110 | return balances[account]; 111 | } 112 | 113 | /** 114 | * @notice Transfer `amount` tokens from `msg.sender` to `dst` 115 | * @param dst The address of the destination account 116 | * @param rawAmount The number of tokens to transfer 117 | * @return Whether or not the transfer succeeded 118 | */ 119 | function transfer(address dst, uint rawAmount) external returns (bool) { 120 | uint96 amount = safe96(rawAmount, "Comp::transfer: amount exceeds 96 bits"); 121 | _transferTokens(msg.sender, dst, amount); 122 | return true; 123 | } 124 | 125 | /** 126 | * @notice Transfer `amount` tokens from `src` to `dst` 127 | * @param src The address of the source account 128 | * @param dst The address of the destination account 129 | * @param rawAmount The number of tokens to transfer 130 | * @return Whether or not the transfer succeeded 131 | */ 132 | function transferFrom(address src, address dst, uint rawAmount) external returns (bool) { 133 | address spender = msg.sender; 134 | uint96 spenderAllowance = allowances[src][spender]; 135 | uint96 amount = safe96(rawAmount, "Comp::approve: amount exceeds 96 bits"); 136 | 137 | if (spender != src && spenderAllowance != uint96(-1)) { 138 | uint96 newAllowance = sub96(spenderAllowance, amount, "Comp::transferFrom: transfer amount exceeds spender allowance"); 139 | allowances[src][spender] = newAllowance; 140 | 141 | emit Approval(src, spender, newAllowance); 142 | } 143 | 144 | _transferTokens(src, dst, amount); 145 | return true; 146 | } 147 | 148 | /** 149 | * @notice Delegate votes from `msg.sender` to `delegatee` 150 | * @param delegatee The address to delegate votes to 151 | */ 152 | function delegate(address delegatee) public { 153 | return _delegate(msg.sender, delegatee); 154 | } 155 | 156 | /** 157 | * @notice Delegates votes from signatory to `delegatee` 158 | * @param delegatee The address to delegate votes to 159 | * @param nonce The contract state required to match the signature 160 | * @param expiry The time at which to expire the signature 161 | * @param v The recovery byte of the signature 162 | * @param r Half of the ECDSA signature pair 163 | * @param s Half of the ECDSA signature pair 164 | */ 165 | function delegateBySig(address delegatee, uint nonce, uint expiry, uint8 v, bytes32 r, bytes32 s) public { 166 | bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this))); 167 | bytes32 structHash = keccak256(abi.encode(DELEGATION_TYPEHASH, delegatee, nonce, expiry)); 168 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); 169 | address signatory = ecrecover(digest, v, r, s); 170 | require(signatory != address(0), "Comp::delegateBySig: invalid signature"); 171 | require(nonce == nonces[signatory]++, "Comp::delegateBySig: invalid nonce"); 172 | require(now <= expiry, "Comp::delegateBySig: signature expired"); 173 | return _delegate(signatory, delegatee); 174 | } 175 | 176 | /** 177 | * @notice Gets the current votes balance for `account` 178 | * @param account The address to get votes balance 179 | * @return The number of current votes for `account` 180 | */ 181 | function getCurrentVotes(address account) external view returns (uint96) { 182 | uint32 nCheckpoints = numCheckpoints[account]; 183 | return nCheckpoints > 0 ? checkpoints[account][nCheckpoints - 1].votes : 0; 184 | } 185 | 186 | /** 187 | * @notice Determine the prior number of votes for an account as of a block number 188 | * @dev Block number must be a finalized block or else this function will revert to prevent misinformation. 189 | * @param account The address of the account to check 190 | * @param blockNumber The block number to get the vote balance at 191 | * @return The number of votes the account had as of the given block 192 | */ 193 | function getPriorVotes(address account, uint blockNumber) public view returns (uint96) { 194 | require(blockNumber < block.number, "Comp::getPriorVotes: not yet determined"); 195 | 196 | uint32 nCheckpoints = numCheckpoints[account]; 197 | if (nCheckpoints == 0) { 198 | return 0; 199 | } 200 | 201 | // First check most recent balance 202 | if (checkpoints[account][nCheckpoints - 1].fromBlock <= blockNumber) { 203 | return checkpoints[account][nCheckpoints - 1].votes; 204 | } 205 | 206 | // Next check implicit zero balance 207 | if (checkpoints[account][0].fromBlock > blockNumber) { 208 | return 0; 209 | } 210 | 211 | uint32 lower = 0; 212 | uint32 upper = nCheckpoints - 1; 213 | while (upper > lower) { 214 | uint32 center = upper - (upper - lower) / 2; // ceil, avoiding overflow 215 | Checkpoint memory cp = checkpoints[account][center]; 216 | if (cp.fromBlock == blockNumber) { 217 | return cp.votes; 218 | } else if (cp.fromBlock < blockNumber) { 219 | lower = center; 220 | } else { 221 | upper = center - 1; 222 | } 223 | } 224 | return checkpoints[account][lower].votes; 225 | } 226 | 227 | function _delegate(address delegator, address delegatee) internal { 228 | address currentDelegate = delegates[delegator]; 229 | uint96 delegatorBalance = balances[delegator]; 230 | delegates[delegator] = delegatee; 231 | 232 | emit DelegateChanged(delegator, currentDelegate, delegatee); 233 | 234 | _moveDelegates(currentDelegate, delegatee, delegatorBalance); 235 | } 236 | 237 | function _transferTokens(address src, address dst, uint96 amount) internal { 238 | require(src != address(0), "Comp::_transferTokens: cannot transfer from the zero address"); 239 | require(dst != address(0), "Comp::_transferTokens: cannot transfer to the zero address"); 240 | 241 | balances[src] = sub96(balances[src], amount, "Comp::_transferTokens: transfer amount exceeds balance"); 242 | balances[dst] = add96(balances[dst], amount, "Comp::_transferTokens: transfer amount overflows"); 243 | emit Transfer(src, dst, amount); 244 | 245 | _moveDelegates(delegates[src], delegates[dst], amount); 246 | } 247 | 248 | function _moveDelegates(address srcRep, address dstRep, uint96 amount) internal { 249 | if (srcRep != dstRep && amount > 0) { 250 | if (srcRep != address(0)) { 251 | uint32 srcRepNum = numCheckpoints[srcRep]; 252 | uint96 srcRepOld = srcRepNum > 0 ? checkpoints[srcRep][srcRepNum - 1].votes : 0; 253 | uint96 srcRepNew = sub96(srcRepOld, amount, "Comp::_moveVotes: vote amount underflows"); 254 | _writeCheckpoint(srcRep, srcRepNum, srcRepOld, srcRepNew); 255 | } 256 | 257 | if (dstRep != address(0)) { 258 | uint32 dstRepNum = numCheckpoints[dstRep]; 259 | uint96 dstRepOld = dstRepNum > 0 ? checkpoints[dstRep][dstRepNum - 1].votes : 0; 260 | uint96 dstRepNew = add96(dstRepOld, amount, "Comp::_moveVotes: vote amount overflows"); 261 | _writeCheckpoint(dstRep, dstRepNum, dstRepOld, dstRepNew); 262 | } 263 | } 264 | } 265 | 266 | function _writeCheckpoint(address delegatee, uint32 nCheckpoints, uint96 oldVotes, uint96 newVotes) internal { 267 | uint32 blockNumber = safe32(block.number, "Comp::_writeCheckpoint: block number exceeds 32 bits"); 268 | 269 | if (nCheckpoints > 0 && checkpoints[delegatee][nCheckpoints - 1].fromBlock == blockNumber) { 270 | checkpoints[delegatee][nCheckpoints - 1].votes = newVotes; 271 | } else { 272 | checkpoints[delegatee][nCheckpoints] = Checkpoint(blockNumber, newVotes); 273 | numCheckpoints[delegatee] = nCheckpoints + 1; 274 | } 275 | 276 | emit DelegateVotesChanged(delegatee, oldVotes, newVotes); 277 | } 278 | 279 | function safe32(uint n, string memory errorMessage) internal pure returns (uint32) { 280 | require(n < 2**32, errorMessage); 281 | return uint32(n); 282 | } 283 | 284 | function safe96(uint n, string memory errorMessage) internal pure returns (uint96) { 285 | require(n < 2**96, errorMessage); 286 | return uint96(n); 287 | } 288 | 289 | function add96(uint96 a, uint96 b, string memory errorMessage) internal pure returns (uint96) { 290 | uint96 c = a + b; 291 | require(c >= a, errorMessage); 292 | return c; 293 | } 294 | 295 | function sub96(uint96 a, uint96 b, string memory errorMessage) internal pure returns (uint96) { 296 | require(b <= a, errorMessage); 297 | return a - b; 298 | } 299 | 300 | function getChainId() internal pure returns (uint) { 301 | uint256 chainId; 302 | assembly { chainId := chainid() } 303 | return chainId; 304 | } 305 | } -------------------------------------------------------------------------------- /contracts/GovernorAlpha.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2020-03-04 3 | */ 4 | 5 | pragma solidity ^0.5.16; 6 | pragma experimental ABIEncoderV2; 7 | 8 | contract GovernorAlpha { 9 | /// @notice The name of this contract 10 | string public constant name = "Compound Governor Alpha"; 11 | 12 | /// @notice The number of votes in support of a proposal required in order for a quorum to be reached and for a vote to succeed 13 | function quorumVotes() public pure returns (uint) { return 400000e18; } // 400,000 = 4% of Comp 14 | 15 | /// @notice The number of votes required in order for a voter to become a proposer 16 | function proposalThreshold() public pure returns (uint) { return 100000e18; } // 100,000 = 1% of Comp 17 | 18 | /// @notice The maximum number of actions that can be included in a proposal 19 | function proposalMaxOperations() public pure returns (uint) { return 10; } // 10 actions 20 | 21 | /// @notice The delay before voting on a proposal may take place, once proposed 22 | function votingDelay() public pure returns (uint) { return 1; } // 1 block 23 | 24 | /// @notice The duration of voting on a proposal, in blocks 25 | function votingPeriod() public pure returns (uint) { return 17280; } // ~3 days in blocks (assuming 15s blocks) 26 | 27 | /// @notice The address of the Compound Protocol Timelock 28 | TimelockInterface public timelock; 29 | 30 | /// @notice The address of the Compound governance token 31 | CompInterface public comp; 32 | 33 | /// @notice The address of the Governor Guardian 34 | address public guardian; 35 | 36 | /// @notice The total number of proposals 37 | uint public proposalCount; 38 | 39 | struct Proposal { 40 | /// @notice Unique id for looking up a proposal 41 | uint id; 42 | 43 | /// @notice Creator of the proposal 44 | address proposer; 45 | 46 | /// @notice The timestamp that the proposal will be available for execution, set once the vote succeeds 47 | uint eta; 48 | 49 | /// @notice the ordered list of target addresses for calls to be made 50 | address[] targets; 51 | 52 | /// @notice The ordered list of values (i.e. msg.value) to be passed to the calls to be made 53 | uint[] values; 54 | 55 | /// @notice The ordered list of function signatures to be called 56 | string[] signatures; 57 | 58 | /// @notice The ordered list of calldata to be passed to each call 59 | bytes[] calldatas; 60 | 61 | /// @notice The block at which voting begins: holders must delegate their votes prior to this block 62 | uint startBlock; 63 | 64 | /// @notice The block at which voting ends: votes must be cast prior to this block 65 | uint endBlock; 66 | 67 | /// @notice Current number of votes in favor of this proposal 68 | uint forVotes; 69 | 70 | /// @notice Current number of votes in opposition to this proposal 71 | uint againstVotes; 72 | 73 | /// @notice Flag marking whether the proposal has been canceled 74 | bool canceled; 75 | 76 | /// @notice Flag marking whether the proposal has been executed 77 | bool executed; 78 | 79 | /// @notice Receipts of ballots for the entire set of voters 80 | mapping (address => Receipt) receipts; 81 | } 82 | 83 | /// @notice Ballot receipt record for a voter 84 | struct Receipt { 85 | /// @notice Whether or not a vote has been cast 86 | bool hasVoted; 87 | 88 | /// @notice Whether or not the voter supports the proposal 89 | bool support; 90 | 91 | /// @notice The number of votes the voter had, which were cast 92 | uint96 votes; 93 | } 94 | 95 | /// @notice Possible states that a proposal may be in 96 | enum ProposalState { 97 | Pending, 98 | Active, 99 | Canceled, 100 | Defeated, 101 | Succeeded, 102 | Queued, 103 | Expired, 104 | Executed 105 | } 106 | 107 | /// @notice The official record of all proposals ever proposed 108 | mapping (uint => Proposal) public proposals; 109 | 110 | /// @notice The latest proposal for each proposer 111 | mapping (address => uint) public latestProposalIds; 112 | 113 | /// @notice The EIP-712 typehash for the contract's domain 114 | bytes32 public constant DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"); 115 | 116 | /// @notice The EIP-712 typehash for the ballot struct used by the contract 117 | bytes32 public constant BALLOT_TYPEHASH = keccak256("Ballot(uint256 proposalId,bool support)"); 118 | 119 | /// @notice An event emitted when a new proposal is created 120 | event ProposalCreated(uint id, address proposer, address[] targets, uint[] values, string[] signatures, bytes[] calldatas, uint startBlock, uint endBlock, string description); 121 | 122 | /// @notice An event emitted when a vote has been cast on a proposal 123 | event VoteCast(address voter, uint proposalId, bool support, uint votes); 124 | 125 | /// @notice An event emitted when a proposal has been canceled 126 | event ProposalCanceled(uint id); 127 | 128 | /// @notice An event emitted when a proposal has been queued in the Timelock 129 | event ProposalQueued(uint id, uint eta); 130 | 131 | /// @notice An event emitted when a proposal has been executed in the Timelock 132 | event ProposalExecuted(uint id); 133 | 134 | constructor(address timelock_, address comp_, address guardian_) public { 135 | timelock = TimelockInterface(timelock_); 136 | comp = CompInterface(comp_); 137 | guardian = guardian_; 138 | } 139 | 140 | function propose(address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas, string memory description) public returns (uint) { 141 | require(comp.getPriorVotes(msg.sender, sub256(block.number, 1)) > proposalThreshold(), "GovernorAlpha::propose: proposer votes below proposal threshold"); 142 | require(targets.length == values.length && targets.length == signatures.length && targets.length == calldatas.length, "GovernorAlpha::propose: proposal function information arity mismatch"); 143 | require(targets.length != 0, "GovernorAlpha::propose: must provide actions"); 144 | require(targets.length <= proposalMaxOperations(), "GovernorAlpha::propose: too many actions"); 145 | 146 | uint latestProposalId = latestProposalIds[msg.sender]; 147 | if (latestProposalId != 0) { 148 | ProposalState proposersLatestProposalState = state(latestProposalId); 149 | require(proposersLatestProposalState != ProposalState.Active, "GovernorAlpha::propose: one live proposal per proposer, found an already active proposal"); 150 | require(proposersLatestProposalState != ProposalState.Pending, "GovernorAlpha::propose: one live proposal per proposer, found an already pending proposal"); 151 | } 152 | 153 | uint startBlock = add256(block.number, votingDelay()); 154 | uint endBlock = add256(startBlock, votingPeriod()); 155 | 156 | proposalCount++; 157 | Proposal memory newProposal = Proposal({ 158 | id: proposalCount, 159 | proposer: msg.sender, 160 | eta: 0, 161 | targets: targets, 162 | values: values, 163 | signatures: signatures, 164 | calldatas: calldatas, 165 | startBlock: startBlock, 166 | endBlock: endBlock, 167 | forVotes: 0, 168 | againstVotes: 0, 169 | canceled: false, 170 | executed: false 171 | }); 172 | 173 | proposals[newProposal.id] = newProposal; 174 | latestProposalIds[newProposal.proposer] = newProposal.id; 175 | 176 | emit ProposalCreated(newProposal.id, msg.sender, targets, values, signatures, calldatas, startBlock, endBlock, description); 177 | return newProposal.id; 178 | } 179 | 180 | function queue(uint proposalId) public { 181 | require(state(proposalId) == ProposalState.Succeeded, "GovernorAlpha::queue: proposal can only be queued if it is succeeded"); 182 | Proposal storage proposal = proposals[proposalId]; 183 | uint eta = add256(block.timestamp, timelock.delay()); 184 | for (uint i = 0; i < proposal.targets.length; i++) { 185 | _queueOrRevert(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], eta); 186 | } 187 | proposal.eta = eta; 188 | emit ProposalQueued(proposalId, eta); 189 | } 190 | 191 | function _queueOrRevert(address target, uint value, string memory signature, bytes memory data, uint eta) internal { 192 | require(!timelock.queuedTransactions(keccak256(abi.encode(target, value, signature, data, eta))), "GovernorAlpha::_queueOrRevert: proposal action already queued at eta"); 193 | timelock.queueTransaction(target, value, signature, data, eta); 194 | } 195 | 196 | function execute(uint proposalId) public payable { 197 | require(state(proposalId) == ProposalState.Queued, "GovernorAlpha::execute: proposal can only be executed if it is queued"); 198 | Proposal storage proposal = proposals[proposalId]; 199 | proposal.executed = true; 200 | for (uint i = 0; i < proposal.targets.length; i++) { 201 | timelock.executeTransaction.value(proposal.values[i])(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta); 202 | } 203 | emit ProposalExecuted(proposalId); 204 | } 205 | 206 | function cancel(uint proposalId) public { 207 | ProposalState state = state(proposalId); 208 | require(state != ProposalState.Executed, "GovernorAlpha::cancel: cannot cancel executed proposal"); 209 | 210 | Proposal storage proposal = proposals[proposalId]; 211 | require(msg.sender == guardian || comp.getPriorVotes(proposal.proposer, sub256(block.number, 1)) < proposalThreshold(), "GovernorAlpha::cancel: proposer above threshold"); 212 | 213 | proposal.canceled = true; 214 | for (uint i = 0; i < proposal.targets.length; i++) { 215 | timelock.cancelTransaction(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], proposal.eta); 216 | } 217 | 218 | emit ProposalCanceled(proposalId); 219 | } 220 | 221 | function getActions(uint proposalId) public view returns (address[] memory targets, uint[] memory values, string[] memory signatures, bytes[] memory calldatas) { 222 | Proposal storage p = proposals[proposalId]; 223 | return (p.targets, p.values, p.signatures, p.calldatas); 224 | } 225 | 226 | function getReceipt(uint proposalId, address voter) public view returns (Receipt memory) { 227 | return proposals[proposalId].receipts[voter]; 228 | } 229 | 230 | function state(uint proposalId) public view returns (ProposalState) { 231 | require(proposalCount >= proposalId && proposalId > 0, "GovernorAlpha::state: invalid proposal id"); 232 | Proposal storage proposal = proposals[proposalId]; 233 | if (proposal.canceled) { 234 | return ProposalState.Canceled; 235 | } else if (block.number <= proposal.startBlock) { 236 | return ProposalState.Pending; 237 | } else if (block.number <= proposal.endBlock) { 238 | return ProposalState.Active; 239 | } else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes()) { 240 | return ProposalState.Defeated; 241 | } else if (proposal.eta == 0) { 242 | return ProposalState.Succeeded; 243 | } else if (proposal.executed) { 244 | return ProposalState.Executed; 245 | } else if (block.timestamp >= add256(proposal.eta, timelock.GRACE_PERIOD())) { 246 | return ProposalState.Expired; 247 | } else { 248 | return ProposalState.Queued; 249 | } 250 | } 251 | 252 | function castVote(uint proposalId, bool support) public { 253 | return _castVote(msg.sender, proposalId, support); 254 | } 255 | 256 | function castVoteBySig(uint proposalId, bool support, uint8 v, bytes32 r, bytes32 s) public { 257 | bytes32 domainSeparator = keccak256(abi.encode(DOMAIN_TYPEHASH, keccak256(bytes(name)), getChainId(), address(this))); 258 | bytes32 structHash = keccak256(abi.encode(BALLOT_TYPEHASH, proposalId, support)); 259 | bytes32 digest = keccak256(abi.encodePacked("\x19\x01", domainSeparator, structHash)); 260 | address signatory = ecrecover(digest, v, r, s); 261 | require(signatory != address(0), "GovernorAlpha::castVoteBySig: invalid signature"); 262 | return _castVote(signatory, proposalId, support); 263 | } 264 | 265 | function _castVote(address voter, uint proposalId, bool support) internal { 266 | require(state(proposalId) == ProposalState.Active, "GovernorAlpha::_castVote: voting is closed"); 267 | Proposal storage proposal = proposals[proposalId]; 268 | Receipt storage receipt = proposal.receipts[voter]; 269 | require(receipt.hasVoted == false, "GovernorAlpha::_castVote: voter already voted"); 270 | uint96 votes = comp.getPriorVotes(voter, proposal.startBlock); 271 | 272 | if (support) { 273 | proposal.forVotes = add256(proposal.forVotes, votes); 274 | } else { 275 | proposal.againstVotes = add256(proposal.againstVotes, votes); 276 | } 277 | 278 | receipt.hasVoted = true; 279 | receipt.support = support; 280 | receipt.votes = votes; 281 | 282 | emit VoteCast(voter, proposalId, support, votes); 283 | } 284 | 285 | function __acceptAdmin() public { 286 | require(msg.sender == guardian, "GovernorAlpha::__acceptAdmin: sender must be gov guardian"); 287 | timelock.acceptAdmin(); 288 | } 289 | 290 | function __abdicate() public { 291 | require(msg.sender == guardian, "GovernorAlpha::__abdicate: sender must be gov guardian"); 292 | guardian = address(0); 293 | } 294 | 295 | function __queueSetTimelockPendingAdmin(address newPendingAdmin, uint eta) public { 296 | require(msg.sender == guardian, "GovernorAlpha::__queueSetTimelockPendingAdmin: sender must be gov guardian"); 297 | timelock.queueTransaction(address(timelock), 0, "setPendingAdmin(address)", abi.encode(newPendingAdmin), eta); 298 | } 299 | 300 | function __executeSetTimelockPendingAdmin(address newPendingAdmin, uint eta) public { 301 | require(msg.sender == guardian, "GovernorAlpha::__executeSetTimelockPendingAdmin: sender must be gov guardian"); 302 | timelock.executeTransaction(address(timelock), 0, "setPendingAdmin(address)", abi.encode(newPendingAdmin), eta); 303 | } 304 | 305 | function add256(uint256 a, uint256 b) internal pure returns (uint) { 306 | uint c = a + b; 307 | require(c >= a, "addition overflow"); 308 | return c; 309 | } 310 | 311 | function sub256(uint256 a, uint256 b) internal pure returns (uint) { 312 | require(b <= a, "subtraction underflow"); 313 | return a - b; 314 | } 315 | 316 | function getChainId() internal pure returns (uint) { 317 | uint chainId; 318 | assembly { chainId := chainid() } 319 | return chainId; 320 | } 321 | } 322 | 323 | interface TimelockInterface { 324 | function delay() external view returns (uint); 325 | function GRACE_PERIOD() external view returns (uint); 326 | function acceptAdmin() external; 327 | function queuedTransactions(bytes32 hash) external view returns (bool); 328 | function queueTransaction(address target, uint value, string calldata signature, bytes calldata data, uint eta) external returns (bytes32); 329 | function cancelTransaction(address target, uint value, string calldata signature, bytes calldata data, uint eta) external; 330 | function executeTransaction(address target, uint value, string calldata signature, bytes calldata data, uint eta) external payable returns (bytes memory); 331 | } 332 | 333 | interface CompInterface { 334 | function getPriorVotes(address account, uint blockNumber) external view returns (uint96); 335 | } -------------------------------------------------------------------------------- /contracts/Timelock.sol: -------------------------------------------------------------------------------- 1 | /** 2 | *Submitted for verification at Etherscan.io on 2019-10-11 3 | */ 4 | 5 | // File: contracts/SafeMath.sol 6 | 7 | pragma solidity ^0.5.8; 8 | 9 | // From https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/Math.sol 10 | // Subject to the MIT license. 11 | 12 | /** 13 | * @dev Wrappers over Solidity's arithmetic operations with added overflow 14 | * checks. 15 | * 16 | * Arithmetic operations in Solidity wrap on overflow. This can easily result 17 | * in bugs, because programmers usually assume that an overflow raises an 18 | * error, which is the standard behavior in high level programming languages. 19 | * `SafeMath` restores this intuition by reverting the transaction when an 20 | * operation overflows. 21 | * 22 | * Using this library instead of the unchecked operations eliminates an entire 23 | * class of bugs, so it's recommended to use it always. 24 | */ 25 | library SafeMath { 26 | /** 27 | * @dev Returns the addition of two unsigned integers, reverting on 28 | * overflow. 29 | * 30 | * Counterpart to Solidity's `+` operator. 31 | * 32 | * Requirements: 33 | * - Addition cannot overflow. 34 | */ 35 | function add(uint256 a, uint256 b) internal pure returns (uint256) { 36 | uint256 c = a + b; 37 | require(c >= a, "SafeMath: addition overflow"); 38 | 39 | return c; 40 | } 41 | 42 | /** 43 | * @dev Returns the subtraction of two unsigned integers, reverting on 44 | * overflow (when the result is negative). 45 | * 46 | * Counterpart to Solidity's `-` operator. 47 | * 48 | * Requirements: 49 | * - Subtraction cannot overflow. 50 | */ 51 | function sub(uint256 a, uint256 b) internal pure returns (uint256) { 52 | return sub(a, b, "SafeMath: subtraction overflow"); 53 | } 54 | 55 | /** 56 | * @dev Returns the subtraction of two unsigned integers, reverting with custom message on 57 | * overflow (when the result is negative). 58 | * 59 | * Counterpart to Solidity's `-` operator. 60 | * 61 | * Requirements: 62 | * - Subtraction cannot overflow. 63 | * 64 | * NOTE: This is a feature of the next version of OpenZeppelin Contracts. 65 | * @dev Get it via `npm install @openzeppelin/contracts@next`. 66 | */ 67 | function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 68 | require(b <= a, errorMessage); 69 | uint256 c = a - b; 70 | 71 | return c; 72 | } 73 | 74 | /** 75 | * @dev Returns the multiplication of two unsigned integers, reverting on 76 | * overflow. 77 | * 78 | * Counterpart to Solidity's `*` operator. 79 | * 80 | * Requirements: 81 | * - Multiplication cannot overflow. 82 | */ 83 | function mul(uint256 a, uint256 b) internal pure returns (uint256) { 84 | // Gas optimization: this is cheaper than requiring 'a' not being zero, but the 85 | // benefit is lost if 'b' is also tested. 86 | // See: https://github.com/OpenZeppelin/openzeppelin-contracts/pull/522 87 | if (a == 0) { 88 | return 0; 89 | } 90 | 91 | uint256 c = a * b; 92 | require(c / a == b, "SafeMath: multiplication overflow"); 93 | 94 | return c; 95 | } 96 | 97 | /** 98 | * @dev Returns the integer division of two unsigned integers. Reverts on 99 | * division by zero. The result is rounded towards zero. 100 | * 101 | * Counterpart to Solidity's `/` operator. Note: this function uses a 102 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 103 | * uses an invalid opcode to revert (consuming all remaining gas). 104 | * 105 | * Requirements: 106 | * - The divisor cannot be zero. 107 | */ 108 | function div(uint256 a, uint256 b) internal pure returns (uint256) { 109 | return div(a, b, "SafeMath: division by zero"); 110 | } 111 | 112 | /** 113 | * @dev Returns the integer division of two unsigned integers. Reverts with custom message on 114 | * division by zero. The result is rounded towards zero. 115 | * 116 | * Counterpart to Solidity's `/` operator. Note: this function uses a 117 | * `revert` opcode (which leaves remaining gas untouched) while Solidity 118 | * uses an invalid opcode to revert (consuming all remaining gas). 119 | * 120 | * Requirements: 121 | * - The divisor cannot be zero. 122 | * NOTE: This is a feature of the next version of OpenZeppelin Contracts. 123 | * @dev Get it via `npm install @openzeppelin/contracts@next`. 124 | */ 125 | function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 126 | // Solidity only automatically asserts when dividing by 0 127 | require(b > 0, errorMessage); 128 | uint256 c = a / b; 129 | // assert(a == b * c + a % b); // There is no case in which this doesn't hold 130 | 131 | return c; 132 | } 133 | 134 | /** 135 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 136 | * Reverts when dividing by zero. 137 | * 138 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 139 | * opcode (which leaves remaining gas untouched) while Solidity uses an 140 | * invalid opcode to revert (consuming all remaining gas). 141 | * 142 | * Requirements: 143 | * - The divisor cannot be zero. 144 | */ 145 | function mod(uint256 a, uint256 b) internal pure returns (uint256) { 146 | return mod(a, b, "SafeMath: modulo by zero"); 147 | } 148 | 149 | /** 150 | * @dev Returns the remainder of dividing two unsigned integers. (unsigned integer modulo), 151 | * Reverts with custom message when dividing by zero. 152 | * 153 | * Counterpart to Solidity's `%` operator. This function uses a `revert` 154 | * opcode (which leaves remaining gas untouched) while Solidity uses an 155 | * invalid opcode to revert (consuming all remaining gas). 156 | * 157 | * Requirements: 158 | * - The divisor cannot be zero. 159 | * 160 | * NOTE: This is a feature of the next version of OpenZeppelin Contracts. 161 | * @dev Get it via `npm install @openzeppelin/contracts@next`. 162 | */ 163 | function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) { 164 | require(b != 0, errorMessage); 165 | return a % b; 166 | } 167 | } 168 | 169 | // File: contracts/Timelock.sol 170 | 171 | pragma solidity ^0.5.8; 172 | 173 | 174 | contract Timelock { 175 | using SafeMath for uint; 176 | 177 | event NewAdmin(address indexed newAdmin); 178 | event NewPendingAdmin(address indexed newPendingAdmin); 179 | event NewDelay(uint indexed newDelay); 180 | event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); 181 | event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); 182 | event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); 183 | 184 | uint public constant GRACE_PERIOD = 14 days; 185 | uint public constant MINIMUM_DELAY = 2 days; 186 | uint public constant MAXIMUM_DELAY = 30 days; 187 | 188 | address public admin; 189 | address public pendingAdmin; 190 | uint public delay; 191 | 192 | mapping (bytes32 => bool) public queuedTransactions; 193 | 194 | 195 | constructor(address admin_, uint delay_) public { 196 | require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); 197 | require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); 198 | 199 | admin = admin_; 200 | delay = delay_; 201 | } 202 | 203 | function() external payable { } 204 | 205 | function setDelay(uint delay_) public { 206 | require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); 207 | require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); 208 | require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); 209 | delay = delay_; 210 | 211 | emit NewDelay(delay); 212 | } 213 | 214 | function acceptAdmin() public { 215 | require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); 216 | admin = msg.sender; 217 | pendingAdmin = address(0); 218 | 219 | emit NewAdmin(admin); 220 | } 221 | 222 | function setPendingAdmin(address pendingAdmin_) public { 223 | require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); 224 | pendingAdmin = pendingAdmin_; 225 | 226 | emit NewPendingAdmin(pendingAdmin); 227 | } 228 | 229 | function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public returns (bytes32) { 230 | require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); 231 | require(eta >= getBlockTimestamp().add(delay), "Timelock::queueTransaction: Estimated execution block must satisfy delay."); 232 | 233 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 234 | queuedTransactions[txHash] = true; 235 | 236 | emit QueueTransaction(txHash, target, value, signature, data, eta); 237 | return txHash; 238 | } 239 | 240 | function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public { 241 | require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); 242 | 243 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 244 | queuedTransactions[txHash] = false; 245 | 246 | emit CancelTransaction(txHash, target, value, signature, data, eta); 247 | } 248 | 249 | function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public payable returns (bytes memory) { 250 | require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); 251 | 252 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 253 | require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); 254 | require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); 255 | require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale."); 256 | 257 | queuedTransactions[txHash] = false; 258 | 259 | bytes memory callData; 260 | 261 | if (bytes(signature).length == 0) { 262 | callData = data; 263 | } else { 264 | callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); 265 | } 266 | 267 | // solium-disable-next-line security/no-call-value 268 | (bool success, bytes memory returnData) = target.call.value(value)(callData); 269 | require(success, "Timelock::executeTransaction: Transaction execution reverted."); 270 | 271 | emit ExecuteTransaction(txHash, target, value, signature, data, eta); 272 | 273 | return returnData; 274 | } 275 | 276 | function getBlockTimestamp() internal view returns (uint) { 277 | // solium-disable-next-line security/no-block-members 278 | return block.timestamp; 279 | } 280 | } -------------------------------------------------------------------------------- /hardhat.config.js: -------------------------------------------------------------------------------- 1 | require("@nomiclabs/hardhat-waffle"); 2 | require("@nomiclabs/hardhat-ethers"); 3 | 4 | 5 | // This is a sample Hardhat task. To learn how to create your own go to 6 | // https://hardhat.org/guides/create-task.html 7 | task("accounts", "Prints the list of accounts", async () => { 8 | const accounts = await ethers.getSigners(); 9 | 10 | for (const account of accounts) { 11 | console.log(account.address); 12 | } 13 | }); 14 | 15 | task("Deploy", "Deploys a COMPound style governance system") 16 | .addParam("token", "The address to receive the initial supply") 17 | .addParam("timelock", "The timelock administrator") 18 | .addParam("guardian", "The governor guardian").setAction(async taskArgs => { 19 | 20 | const { deploy } = require("./scripts/Deploy"); 21 | 22 | await deploy({ 23 | tokenRecipient: taskArgs.token, 24 | timeLockAdmin: taskArgs.timelock, 25 | guardian: taskArgs.guardian 26 | }); 27 | 28 | }) 29 | 30 | 31 | // You need to export an object to set up your config 32 | // Go to https://hardhat.org/config/ to learn more 33 | 34 | /** 35 | * @type import('hardhat/config').HardhatUserConfig 36 | */ 37 | module.exports = { 38 | networks: { 39 | rinkeby: { 40 | url: "https://rinkeby.infura.io/v3/<>", 41 | accounts: ["0xAPrivateKey"] 42 | } 43 | }, 44 | solidity: "0.5.16", 45 | }; 46 | 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tutorial-governance", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "Dennison Bertram ", 11 | "license": "MIT", 12 | "devDependencies": { 13 | "@nomiclabs/hardhat-ethers": "^2.0.1", 14 | "@nomiclabs/hardhat-waffle": "^2.0.1", 15 | "chai": "^4.2.0", 16 | "ethereum-waffle": "^3.2.1", 17 | "ethers": "^5.0.24", 18 | "hardhat": "^2.0.6" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /scripts/Deploy.js: -------------------------------------------------------------------------------- 1 | 2 | const hre = require("hardhat"); 3 | const ethers = hre.ethers; 4 | 5 | async function main({tokenRecipient, timeLockAdmin, guardian}) { 6 | 7 | // Compile our Contracts, just in case 8 | await hre.run('compile'); 9 | 10 | // This gets the contract from 11 | const Token = await hre.ethers.getContractFactory("Comp"); 12 | const token = await Token.deploy(tokenRecipient); 13 | await token.deployed(); 14 | await token.deployTransaction.wait(); 15 | 16 | // Deploy Timelock 17 | const delay = 172800; 18 | const Timelock = await ethers.getContractFactory("Timelock"); 19 | const timelock = await Timelock.deploy(timeLockAdmin, delay); 20 | await timelock.deployed(); 21 | await timelock.deployTransaction.wait(); 22 | 23 | // Deploy Governance 24 | const Gov = await ethers.getContractFactory("GovernorAlpha"); 25 | const gov = await Gov.deploy(timelock.address, token.address, guardian); 26 | await gov.deployed(); 27 | await gov.deployTransaction.wait(); 28 | 29 | console.log(`Token deployed to: ${token.address}`); 30 | console.log(`TimeLock deployed to: ${timelock.address}`); 31 | console.log(`GovernorAlpha deployed to: ${gov.address}`) 32 | 33 | const initialBalance = await token.balanceOf(tokenRecipient); 34 | console.log(`${initialBalance / 1e18} tokens transfered to ${tokenRecipient}`); 35 | } 36 | 37 | module.exports = { 38 | deploy: main 39 | } -------------------------------------------------------------------------------- /test/sample-test.js: -------------------------------------------------------------------------------- 1 | const { expect } = require("chai"); 2 | 3 | describe("Greeter", function() { 4 | it("Should return the new greeting once it's changed", async function() { 5 | const Greeter = await ethers.getContractFactory("Greeter"); 6 | const greeter = await Greeter.deploy("Hello, world!"); 7 | 8 | await greeter.deployed(); 9 | expect(await greeter.greet()).to.equal("Hello, world!"); 10 | 11 | await greeter.setGreeting("Hola, mundo!"); 12 | expect(await greeter.greet()).to.equal("Hola, mundo!"); 13 | }); 14 | }); 15 | --------------------------------------------------------------------------------