Build a decentralized governance model from scratch with hardhat, TypeScript, solidity, and Openzeppelin. This DAO (Decentralized Autonomous Organization) uses an ERC20 token with voting power to make decisions
How to build/code a DAO
Introduction
We are going to learn how to build an on-chain DAO using hardhat, solidity, typescript, and Openzeppelin. For those of you who don’t know, a DAO is a decentralized autonomous organization that is typically powered by the blockchain. You can watch my previous video overview about the DAO tooling landscape or read my previous article on How to build a DAO (High Level).
For this one, we are jumping right into the code. We will be using a 100% on-chain governance model, using an ERC20 token to vote for proposed changes. Once tooling improves for off-chain voting (using the Chainlink OCR model and a decentralized database like IPFS), we’ll probably have a tutorial to do that to save gas.
But to reiterate, be 100% sure to read my previous article on the landscape of the space before following here! Additionally, if you want to see a pythonic version with brownie, you can watch the video here, and see the code here.
Hardhat Video:
How to Code a DAO
Quickstart
The quickest way to get everything is to just do the following:
git clone https://github.com/PatrickAlphaC/dao-templatecd dao-templateyarnyarn hardhat test
And boom! You’ll run through the tests that mock proposing a vote, voting on a vote, queueing the vote, then executing!
Here is the rundown of what the test suite does:
- We deploy an ERC20 token that we will use to govern our DAO.
- We deploy a Timelock contract that we will use to give a buffer between executing proposals.
Note: The timelock is the contract that will handle all the money, ownerships, etc
3. We deploy our Governor contract
Note: The Governance contract is in charge of proposals and such, but the Timelock executes!
4. We deploy an example Box contract, which will be owned by our governance process! (aka, our timelock contract).
5. We propose a new value to be added to our Box contract.
6. We then vote on that proposal.
7. We then queue the proposal to be executed.
8. Then, we execute it!
But, let’s break it down for you…
Getting Started
It’s recommended that you’ve gone through the hardhat getting started documentation before proceeding here.
Requirements
- git: You’ll know you did it right if you can run
git --version
and you see a response likegit version x.x.x
- Nodejs: You’ll know you’ve installed nodejs right if you can run
node --version
and get an output like:vx.x.x
- Yarn instead of
npm
: You’ll know you’ve installed yarn right if you can run:yarn --version
And get an output like:x.x.x
. You might need to install it with npm
What we are building
We are going to be building a DAO that uses ERC20 tokens to vote on our basic Box.sol
contract that looks like this:
The beauty of how this all works is that governance is modular, and can be “stuck on” to really any contract. The key here is that our contract is “ownable”, which means that only the owner can call the store
function. And the owner of this contract is going to be our DAO!
Build It
To get started, set up a TypeScript hardhat project:
mkdir dao-templatecd dao-templateyarn add hardhatyarn hardhat
And select the TypeScript option. This will create a few folders and files in your directory to play with.
In your contracts
folder, create a file called Box.sol
. And add the Box
code you see from above, this will be the contract that we “do” governance on.
In our hardhat.config.ts
we will want to update the solidity version to 0.8.12
or anything above 0.8.4
.
We’ll need to add openzeppelin contracts, and then try to compile with:
yarn add @openzeppelin/contractsyarn hardhat compile
And it should compile successfully! We have a box… Now what?
The Governance Token
Our governance token is going to be a little special, create a new file called GovernanceToken.sol
in your contracts
folder. It should look like this:
You’ll notice this isn’t a “normal” ERC20 token, this is because we need to keep track of “snapshots.” Whenever a vote is proposed, we want to make sure that we use people's balances from X blocks ago, instead of whenever the proposal was made. This will reduce people buying and selling voting tokens whenever they think a vote they want to be a part of is coming up and will make sure the number of votes stays consistent.
Once a “checkpoint” or a “snapshot” of people's tokens balances are calculated for a voting period, that’s it! You can’t buy more tokens after a vote is proposed and get more votes! You would have had to have already been holding the token.
We can make sure this is compiling with:
yarn hardhat compile
The Governor Contract
Not to be confused with the governator
Now, let’s create a folder in our contracts
folder called governance_standard
. In the future, I want to add a governance_offchain
folder, but until that Chainlink bit is in, this is what we got!
We’ll create a contract called GovernorContract.sol
that looks like:
This is the contract that facilitates the voting of our GovernorToken. Here are the main functions we look at:
propose
: Proposes a transaction. The propose function is modular in the sense that it allows you to call any transaction on any contract. The parameters are:
targets
: A list of addresses you want to call some function on.values
: A list of ETH (or Layer 1 crypto) you want to send with your transactions to each address accordingly.calldatas
: A list of encoded functions and arguments of each function you want to call on each address.description
: The description of the proposal you’re using.
The beauty of this function is it allows you do to nearly ANYTHING across many addresses in a single transaction.
castVote
: How we cast votes.
queue
: Once a vote passes, we queue it to be executed.
execute
: After the time lock is over, we execute the proposal.
You’ll notice that once a vote passes, it doesn’t go into effect right away, this is intentional. We want to give people time to “get out” of a protocol if they don’t like a change that is made. This “time lock” or “time out” is enforced by our “Time lock” contract… which we make next!
The Time Lock
Create a new file in the same folder as our governance contract called TimeLock.sol
. This is the contract that will “own” the Box.
Note: Yes, you read that right. The TimeLock owns everything. This is because whenever our governance passes something, we want to make sure that we wait for a minimum delay before executing that function. The time lock enforces this. The governor contract will be the only contract that can ask the timelock contract to “do” stuff. And it will only be able to ask if a vote passes!
Our time lock has a few parameters:
minDelay
: How long we should wait between a vote passing and it executing.proposers
: Who can propose transactions to the TimeLock contract (we will set it so only the governance contract can).executors
: We set this up so anyone can execute a function that has passed and has waited out the time. However, this would be a perfect time to add Chainlink Keepers to make sure execution is decentralized!
And that’s all the solidity you need! Run yarn hardhat compile
to compile everything!
Scripts and Tests
Now, I don’t want to dump a bunch more code into this article, but that’s the gist of it. You can see the scripts and tests in my dao-template github repo which has scripts called vote
, propose
, and queueAndExecute
which do exactly as their names imply.
You can see the code and see exactly how to do a number of advanced solidity/hardhat concepts like:
- Auto-verify your smart contracts on etherscan
- Fast-forward time on a local network
- Fast-forward blocks on a local network
- How to encode functions and their arguments down to bytes
- Spin up a local hardhat node with all the contracts you like in a single command line
- Advanced gas reporting
- Typechain use
And more!
Learn More
If you’d like to keep getting the most up-to-date smart contract/blockchain/web3 developer content, be sure to follow me on Medium, Twitter, and YouTube to stay up to date.