This article will help you understand the concept of a DAO and help you build a basic DAO.
What are DAOs?
You can think of a DAO as an internet-based entity (like a business) that is collectively owned and governed by its shareholders (members with tokens and proportional voting rights). In a DAO, decisions are made through proposals, and members of the DAO can vote on these proposals, which are then implemented.
The DAO is governed entirely by publicly viewable/verifiable code, with no single person (like the CEO) responsible for making decisions.
How do DAOs work?
As mentioned earlier, DAOs are managed by code, but what if the person on the machine running the code decides to shut down the machine or edit the code?
What is needed is to have the same code run on a set of machines hosted by different entities so that even if one goes down, the other can take over. Blockchains help us solve the above problems, EVM-based blockchains such as Ethereum and Polygon allow us to run smart contracts on public decentralized ledgers. A smart contract deployed on these networks will propagate to all nodes on the network that can view and verify it, and no single party controls the network.
A DAO with token membership issues tokens to its members, which represent voting rights in the system. Depending on the governance set up, anyone can create a DAO change proposal and submit it to a ballot with a quorum (minimum percentage/votes required to pass) and a voting duration. Members can view and vote on proposals, and voting rights are proportional to the number of tokens owned by members. After the voting period is over, we check if the proposal passed, and if so, execute it.
Some examples of DAOs are MakerDAO and Aragon .
The diagram below shows the flow.
let's start building
We'll be using OpenZeppelin contracts in our codebase , and I'll also be using some code from Patrick Collins' DAO template .
prerequisites
You will need the following to get started.
- Node.js: You can download the latest version from the Node.js website . The version I am using at the time of writing this article is 16.14.2.
- Yarn : We will use Yarn as the package manager.
- Hardhat : We will use Hardhat as a local development environment.
database
I've written the code and pushed it to GitHub, if you want to try it yourself you can get the code here , though I suggest you stick around as I'll explain the code.
Scenes
We'll build a DAO that will do the following:
scene 1
- Add initial members. (Let's call them founders).
- Let the founders create a proposal. (Propose a function to be executed on the smart contract).
- Let Founder vote on the above proposal, since Founder has 100% vote share, so it will pass.
- Execute the proposal. (and functions in smart contracts)
scene 2
- Add an initial member (let's call them Founder).
- Add another member and issue them new tokens worth 20% of the founders' share.
- Let the founder create a proposal (propose a function to be executed on the smart contract).
- Let founders and new members vote on the above proposals. The quorum is set to 90%.
- Execution of proposals (and functions in smart contracts).
contract
As mentioned earlier, we will be using OpenZeppelin's governance contract. The contract is as follows:
- Governor contract: The Governor contract determines the number of votes/percentage required for a quorum (for example, if the quorum is 4%, then only 4% of voters need to vote to pass the proposal), the voting cycle is how long the voting is open, and the voting delay is the proposal How long after creation members are allowed to change the amount of tokens they own. Governors also provide functionality to create proposals, vote on them, and execute them.
- TimeLock: The TimeLock contract provides time for members who disagree to exit the system before the decision is executed.
- Token: Token contract is a special type of ERC20 contract that implements ERC20Votes extension. This allows voting rights to be mapped to snapshots of past balances rather than current balances, which helps prevent members from knowing that important proposals are coming and trying to increase their voting power by buying more tokens and then dumping them afterwards.
- Goal: This is the contract whose code will be executed after the proposal passes the vote.
the code
Let's start putting this all together. Create an empty example project using Hardhat. Run the following command in your terminal.
yarn add — development hardhat
Next, let's create our folder structure using hardhat.
You should see a prompt like this
Click Create Basic Example Project. When the process is complete, you should see something like this.
contract
Let's start adding contracts, first let's add the GovernorContract. We can get the same code from OpenZeppelin , or you can copy the code below or copy it from my repo. My contract code fixes a bug in the OpenZeppelin version , as well as parameterization of voting delay, quorum and voting period, similar to the Patrick Collins version.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import “@openzeppelin/contracts/governance/Governor.sol”;
import “@openzeppelin/contracts/governance/extensions/GovernorSettings.sol”;
import “@openzeppelin/contracts/governance/extensions/GovernorCountingSimple.sol”;
//import “@openzeppelin/contracts/governance/extensions/GovernorVotes.sol”;
import “@openzeppelin/contracts/governance/extensions/GovernorVotesQuorumFraction.sol”;
import “@openzeppelin/contracts/governance/extensions/GovernorTimelockControl.sol”;
contract GovernorContract is Governor, GovernorSettings, GovernorCountingSimple, GovernorVotes, GovernorVotesQuorumFraction, GovernorTimelockControl {
constructor(IVotes _token, TimelockController _timelock,uint256 _quorumPercentage,
uint256 _votingPeriod,
uint256 _votingDelay)
Governor(“GovernorContract”)
GovernorSettings(_votingDelay,_votingPeriod,0)
GovernorVotes(_token)
GovernorVotesQuorumFraction(_quorumPercentage)
GovernorTimelockControl(_timelock)
{}
// The following functions are overrides required by Solidity.
function votingDelay()
public
view
override(IGovernor, GovernorSettings)
returns (uint256)
{
return super.votingDelay();
}
function votingPeriod()
public
view
override(IGovernor, GovernorSettings)
returns (uint256)
{
return super.votingPeriod();
}
function quorum(uint256 blockNumber)
public
view
override(IGovernor, GovernorVotesQuorumFraction)
returns (uint256)
{
return super.quorum(blockNumber);
}
function getVotes(address account, uint256 blockNumber)
public
view
override(Governor, IGovernor)
returns (uint256)
{
return _getVotes(account, blockNumber, _defaultParams());
}
function state(uint256 proposalId)
public
view
override(Governor, GovernorTimelockControl)
returns (ProposalState)
{
return super.state(proposalId);
}
function propose(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, string memory description)
public
override(Governor, IGovernor)
returns (uint256)
{
return super.propose(targets, values, calldatas, description);
}
function proposalThreshold()
public
view
override(Governor, GovernorSettings)
returns (uint256)
{
return super.proposalThreshold();
}
function _execute(uint256 proposalId, address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
internal
override(Governor, GovernorTimelockControl)
{
super._execute(proposalId, targets, values, calldatas, descriptionHash);
}
function _cancel(address[] memory targets, uint256[] memory values, bytes[] memory calldatas, bytes32 descriptionHash)
internal
override(Governor, GovernorTimelockControl)
returns (uint256)
{
return super._cancel(targets, values, calldatas, descriptionHash);
}
function _executor()
internal
view
override(Governor, GovernorTimelockControl)
returns (address)
{
return super._executor();
}
function supportsInterface(bytes4 interfaceId)
public
view
override(Governor, GovernorTimelockControl)
returns (bool)
{
return super.supportsInterface(interfaceId);
}
}
Next, let's add the token contract, which is also available on OpenZeppelin. My code has an additional "issueToken" function (more on that later).
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
contract MyToken is ERC20, ERC20Permit, ERC20Votes {
constructor() ERC20("MyToken", "MTK") ERC20Permit("MyToken") {
_mint(msg.sender, 1000);
}
// The functions below are overrides required by Solidity.
function _afterTokenTransfer(address from, address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._mint(to, amount);
}
function _burn(address account, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._burn(account, amount);
}
function issueToken(address to, uint256 amount) public{
_mint(to, amount);
}
}
Finally, let's examine the Target contract, in our case we'll use the same Box contract that Patrick Collins used.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/governance/TimelockController.sol";
contract TimeLock is TimelockController {
// minDelay is how long you have to wait before executing
// proposers is the list of addresses that can propose
// executors is the list of addresses that can execute
constructor(
uint256 minDelay,
address[] memory proposers,
address[] memory executors
) TimelockController(minDelay, proposers, executors) {}
}
test
Now that we have the contract, we need to write the tests. Create a file "sample-test.js" under the "test" folder. Let's start writing our tests. First, let's create a configuration file called "helper.config.js" with the following data.
module.exports=
{
MIN_DELAY:3600,
QUORUM_PERCENTAGE:90,
VOTING_PERIOD:5,
VOTING_DELAY:3,
ADDRESS_ZERO :"0x0000000000000000000000000000000000000000"
}
The quorum is 90%, the voting period is 5 blocks, and the voting delay is 3 blocks. The minimum delay for TimeLock is 3600 seconds.
Let's write the code to deploy all contracts to the local network (Hardhat manages this internally, we don't need to start any process)
governanceToken = await ethers.getContractFactory("MyToken")
deployedToken=await governanceToken.deploy();
await deployedToken.deployed();
transactionResponse = await deployedToken.delegate(owner.address)
await transactionResponse.wait(1)
timeLock = await ethers.getContractFactory("TimeLock")
deployedTimeLock=await timeLock.deploy(MIN_DELAY,[],[]);
await deployedTimeLock.deployed();
governor = await ethers.getContractFactory("GovernorContract")
deployedGovernor=await governor.deploy(deployedToken.address,deployedTimeLock.address,QUORUM_PERCENTAGE,VOTING_PERIOD,VOTING_DELAY);
await deployedGovernor.deployed()
box = await ethers.getContractFactory("Box")
deployedBox=await box.deploy()
await deployedBox.deployed()
proposal creation
Next, create a proposal. We pass the encoded value of the function that will be called on the Box contract and its parameters.
The output of the propose function is a transaction containing the Proposal Id. This is used to track proposals.
const proposalDescription="propose this data"
let encodedFunctionCall = box.interface.encodeFunctionData("store", [77])
const proposeTx = await deployedGovernor.propose([deployedBox.address],[0],[encodedFunctionCall],proposalDescription);
The proposal is to trigger the storage function on the Box contract with value 77 .
vote
We then vote on the proposal, voting "1" for yes.
Note: In this case we have only one member (with 100% of the votes) voting.
const voteWay = 1
const reason = "I vote yes"
let voteTx = await deployedGovernor.castVoteWithReason(proposalId, voteWay, reason)
Queue and Execution
Next, any member from the DAO can queue and execute the proposal, and if the proposal passes the vote, it will be executed and the stored function on the Box contract will be called with a value of 77. You may have noticed that things like moveTime and moveBlocks, these from the Patrick Collins DAO template , can be used in the development environment to simulate the passage of time and block mining, they help us simulate the completion of voting periods, timelock delays, etc.
const queueTx = await deployedGovernor.queue([deployedBox.address],[0],[encodedFunctionCall],descriptionHash)
await queueTx.wait(1)
await moveTime(MIN_DELAY + 1)
await moveBlocks(1)
console.log("Executing...")
const executeTx = await deployedGovernor.execute(
[deployedBox.address],
[0],
[encodedFunctionCall],
descriptionHash
)
await executeTx.wait(1)
const value=await deployedBox.retrieve();
console.log(value)
run test
We can now run the tests with the command
Yarn Hard Hat Test
Issue tokens to new members
What we saw above is the flow of Scenario 1. For Scenario 2, we need to issue new tokens to new members and let them vote on proposals.
The code for issuing tokens is as follows
[owner, addr1, addr2] = await ethers.getSigners();
const signer=await ethers.getSigner(addr1.address);
const deployedTokenUser2=await deployedToken.connect(signer)
await deployedTokenUser2.issueToken(addr1.address,200)
The function getSigners() returns a list of all accounts in the Hardhat development environment, and then we issue 200 tokens to this address.
new member vote
Now that we have another member, we can use him to vote, but the new member cannot vote unless he first adds himself as a delegate to the token contract, this is done so that members who own tokens but don't want to participate in the decision-making don't Additional gas costs are spent to maintain a snapshot of their voting rights on the ledger.
The code for self-delegation is as follows.
[owner, addr1, addr2] = await ethers.getSigners();
const signer=await ethers.getSigner(addr1.address);
const deployedTokenUser2=await deployedToken.connect(signer)
await deployedTokenUser2.issueToken(addr1.address,200)
Article source: https://blog.blockmagnates.com/how-to-build-a-dao-decentralized-autonomous-organization-in-solidity-af1cf900d95d
Get more blockchain learning materials through Github!