Disclaimer: this post and accompanying code is meant for educational purposes only. None of this is audited, and you should seek out audits before using contracts in production.
There has been much ado about DAOs lately--they are being used to govern financial protocols, form modern day guilds, and crowdfund the sale of historical documents.
But how do they work on-chain? This post will walk through an example of setting up governance using a variant of the popular governance contracts created by Compound Finance. The task? Deploying the U.S. federal government as smart contracts on Ethereum.
All the code discussed in this post can be found in a GitHub repo here. Hat tip to OpenZeppelin for an excellent and extensive library of contracts that I use heavily in this tutorial.
The first step in on-chain governance is for determining who will be doing the governing! For the federal government, that will be Congress.
DAO voting power is typically determined by one’s balance of an ERC20 token. To create our governance token, which I’ll be calling CongressToken
, we’ll use OpenZeppelin’s standard ERC20 implementation as well as an extension they’ve made called ERC20Votes.
ERC20Votes
will give us the ability to keep track of voting power and exposes an interface to delegate the voting power of your tokens to yourself or someone else.
Two items to note here. First, in the constructor, I mint 3 tokens (technically there should be 535, one for each member of Congress, but I’m too lazy to simulate more than 3 members) and transfer them to the deployer address.
_mint(msg.sender, 3);
Since there is no publicly available mint()
function, this will be the total supply of CONGRESS forever. I leave it up to the deployer to distribute these tokens, which is not what you want to do in prod. It would be more appropriate to grant the tokens to a separate contract that will distribute tokens according to some rule.
Second item to note is that I set decimals()
to 0. Token balances are always stored as an integer on-chain, and the ERC20 standard dictates that you specify the number of decimal places used to convert these integers into the desired fractional units. 18 decimals is most common, as that is used by ether itself. Here, I don’t desire any ability to fractionalize votes (it’ll be one senator, one vote), so I set decimals to 0.
Next we need to create the logic for governance--how do proposals get created and voted on?
For this we use the OpenZeppelin Governor contract. It’s based on the popular Compound governance contracts, which are also used by Uniswap. Additionally, OpenZeppelin has made an extension called GovernorCompatabilityBravo, which can make the interface of the governance contract compatible with Compound’s GovernorBravo.
There are two constructor arguments--the first (_token
) is an ERCVotes
-compatible ERC20 that will serve as the governance token. For my setup this will be the address of CongressToken
. The second (_timelock
) is address of a timelock contract, which we’ll get to shortly.
I set a few constants in the contract. GovernorVotesQuorumFraction
controls what percent of the total supply of the governance token needs to vote in order for a proposal to pass. I (somewhat arbitrarily) have 4%.
GovernorVotesQuorumFraction(4)
The votingDelay
is the amount of time between when a proposal is created and when voting begins, specified in terms of number of blocks. This gives governance token holders time to review the proposal and delegate their voting power if they choose to do so. I set this at a low value (2 blocks), but this should probably be a day or two in production systems.
The votingPeriod
is the amount of time allocated for voting in terms of number of blocks. Addresses with delegated voting power would need to submit an on-chain vote transaction during that amount of time. I again set this to a low value (100 blocks), but this should be probably 3-5 days in a production system for sensitive items.
Finally, I set the proposalThreshold
. This ensures that not just anyone can submit a proposal--you need to have a certain amount of delegated voting power. Here, I choose 1, meaning any member of congress (delegated CONGRESS voting power of 1) can submit a proposal.
Many DeFi protocol governance systems include a timelock contract. This contract is the one that has the ability to update critical systems, and ensures that no changes can be made without some delay that allows affected users to withdraw funds, change parameters, etc. before a change is made.
For this contract I simply use OpenZeppelin’s TimelockController, which has 3 constructor arguments. The first, _minDelay
, specifies the amount of time (in seconds this time) that must pass before a change is put into effect. The second and third are arrays of addresses having to do with who can affect changes. _proposers
can queue a change and _executors
can enact the change.
We’ll get into how to set those values when we get into the deployment.
Every governance system needs something to govern!
In this setup, the federal government manages an ERC20 contract representing the US Dollar.
I again use OpenZeppelin’s standard ERC20 implementation, but I don’t use the ERC20Votes
extension, because USD won’t be a governance token. I make use of OpenZeppelin’s Ownable contract.
Ownable
allows you to restrict certain functions to be used by an admin only. The default owner is the contract deployer. In the constructor, I add an argument to specify the owner to override the default behavior. I’ll make the timelock contract the owner during deployment.
I add two public functions here, mint
and burn
, that have the modifier onlyOwner
. This will give the federal government the ability to mint and burn USD by proposal.
Generally, governance should have the ability to call certain admin functions (like mint
and burn
in this case).
In the accompanying repo, I include a script called deploy_util that sets up the governance system.
The first step is to deploy the governance token:
This is straightforward--I print out the deployed address along with decimals and total supply.
Next step is to deploy the timelock contract, as it will be an input argument for the governance contract.
Note that I leave _proposers
empty here. The governance contract will be the only proposer, but we do not yet know its address. For _executors
, I include only the zero address (0x000…) which means any address can execute. This isn’t insecure, since only transactions that are proposed can be executed, and only governance can propose.
Next I deploy the governor contract, passing in the addresses for the governance token and the timelock.
At this point the deployer address is an admin on the timelock. I grant the FederalGovernment
contract the role of proposer, and then renounce the deployer’s admin privilege on the timelock so that governance is in control.
Finally, I deploy the USDollar
contract, passing in the timelock contract as the owner. With that, we’re ready for governance!
Now we can see what the governance system can do.
The repo contains another script called proposal_scenario that walks through the creation of a proposal in a local hardhat environment.
First, some housekeeping. I need to distribute the CONGRESS tokens to some senators. I grab three spare addresses to have 3 senators (redSenator
, blueSenator
, and swingSenator
--think Joe Manchin). In order to vote later, the token balances must be delegated to a voter. In this case, all the senators delegate to themselves.
Now we’re ready to create a proposal.
redSenator
wants to propose that $2M is minted, and $1M goes to them and $1M goes to swingSenator
.
The FederalGovernment
contract interface has a propose
function, whereby you can pass calldata to be executed by the timelock.
I create the calldata for the 2 calls to mint on the USDollar contract, and add them to the proposal. The proposal also needs the address to call for each calldata (the USDollar contract for both in this case), the amount of ETH to be sent with the call (0 in this case), and a description.
Once we create the proposal, the governance contract recognizes it as “pending.”
We can’t quite vote on the proposal yet. You might remember that I set reviewPeriod
value on the governance contract that requires n
blocks pass before voting begins, giving folks a chance to delegate if they choose to do so.
Since this is a local environment, we can speed through it.
Now the senators can vote. Given that they’ll receive $1M each, redSenator
and swingSenator
are strongly in favor. blueSenator
, who will receive nothing, opposes.
If we call the governance contract, we can see the votes recorded.
Before we can queue up the proposal with the timelock, the voting period must finish. You may recall that I set this to 100 blocks in the governance contract.
Once again, we speed through. Since the yeas have it, the proposal is in the “succeeded” state.
The next step is to queue the proposal with the timelock. The governance contract allows anyone to do this once the voting process is done, so I have swingSenator
do the honors.
The final step is to go live with the changes. We have another delay, this time enforced by the timelock contract.
After the time has passed, anyone can execute the proposal. I have the original contract deployer do so.
The USD is successfully minted, governance in action!
I hope you found this tutorial helpful!
To recap, we:
Here are links for your reference: