Introduction
Hey there! This tutorial will delve into the creation of a decentralized autonomous organization (DAO) on the Celo blockchain. Part 1 of this tutorial will provide step-by-step guidance on creating the DAO with the help of smart contracts, and deploying the smart contract.
This tutorial is designed for developers who wish to gain practical experience in the web3 space on the Celo blockchain. we’ll be using Solidity, a programming language, to write the smart contract By the end of this tutorial , you will have a comprehensive understanding of building a DAO on Celo, enabling you to explore more advanced applications on the Celo blockchain.
Prerequisite
Before we dive into building the DAO, there are some prerequisites that you need to have:
- Basic knowledge of Hardhat
- Familiarity with the Solidity
- A code editor
Requirements
- Node js
- Solidity
- Hardhat
Writing the Smart Contracts
In this step, we will be creating our token and dao contracts respectively, To begin, let’s create a new file in the hardhat folder called Token.sol and we can add the following code
- Token.sol
This contract creates a new ERC20 token called “Token” with the symbol “TOKEN” and mints 1,000,000 tokens based on the ERC20 standard extended from the OpenZeppelin library.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
// SPDX-License-Identifier: MIT
specifies the license under which the code is released.
The pragma solidity ^0.8.17;
statement specifies the version of the Solidity compiler to be used.
The import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
statement imports the ERC20.sol file from the OpenZeppelin library. This file contains the implementation of the ERC20 token standard.
contract Token is ERC20 {
constructor() ERC20("Token", "TOKEN") {
_mint(msg.sender, 1000000);
}
}
The constructor() ERC20("Token", "TOKEN") { ... }
is the constructor function for the Token
contract. It is executed only once when the contract is deployed. Within the constructor:
The ERC20("Token", "TOKEN")
calls the constructor of the ERC20 contract and sets the token name to Token
and the symbol to TOKEN
.
The _mint(msg.sender, 1000000)
statement mints 1,000,000 tokens and assigns them to the address that deployed the contract (msg.sender).
Your final code should look like this
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Token is ERC20 {
constructor() ERC20("Token", "TOKEN") {
_mint(msg.sender, 1000000);
}
}
For the Dao contract, create a new file called Dao.sol
- Dao.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract DAO {
IERC20 public token;
uint256 public proposalIndex;
event winner(uint256 _index, bytes32 proposal, Vote winningVote);
mapping(uint256 => Proposal) public proposals;
Here, we declared our Dao contract , the IERC20 public token
is a public variable, representing an ERC20 token contract. It will be used to interact with the token within the DAO.
uint256 public proposalIndex
is a public variable representing the index of the current proposal. It will be incremented for each new proposal added to the DAO.
event winner(uint256 _index, bytes32 proposal, Vote winningVote):
emits the declaration of an event that will be triggered when a proposal is determined.
constructor(address _token) {
token = IERC20(_token);
}
The constructor
includes an address parameter _token, which signifies the address of an ERC20 token contract.
struct Proposal {
uint256 accept;
uint256 reject;
uint256 abstain;
uint256 deadline;
bytes32 title;
mapping(address => bool) voted;
}
The struct
allows the DAO contract to manage and track the details of each proposal, including vote counts, deadlines, titles, and voter participation.
mapping(address => bool) voted
maintains a record of whether an address has participated in voting for the proposal. It associates each voter’s address with a boolean value that signifies whether they have already cast their vote or not.
enum Vote {
accept,
reject,
abstain
}
Here, we define an enum
called Vote
which has three possible values
accept
, reject
and abstain
.
modifier onlyTokenHolders() {
require(
token.balanceOf(msg.sender) > 0,
"NOT_A_TOKEN_HOLDER"
);
_;
}
The modifier
verifies that only addresses possessing a balance greater than zero are permitted to interact with other sections of the smart contract. Otherwise, it will display the error message “NOT_A_TOKEN_HOLDER.”
function createProposal(bytes32 _proposal) public onlyTokenHolders returns(uint256) {
Proposal storage proposal = proposals[proposalIndex];
proposal.title = _proposal;
proposal.deadline = block.timestamp + 1 days;
proposalIndex++;
return proposalIndex - 1;
}
The createProposal
function allows token holders to create proposals by providing a title and setting a deadline for the proposal. The proposals are stored in the mapping (proposals) we created earlier on.
function voteOnProposal(uint256 _index, Vote _vote) public onlyTokenHolders {
Proposal storage proposal = proposals[_index];
require (proposal.deadline > block.timestamp, "Voting has ended");
require(proposal.voted[msg.sender] == false, "you have voted");
proposal.voted[msg.sender] = true;
if (_vote == Vote.accept) {
proposal.accept += token.balanceOf(msg.sender);
} else if (_vote == Vote.reject) {
proposal.reject += token.balanceOf(msg.sender);
} else if (_vote == Vote.abstain) {
proposal.abstain += token.balanceOf(msg.sender);
}
}
The voteOnProposal
function enables token holders to cast their votes on a particular proposal within the smart contract. Voters have the option to select one of the three available votes: accept, reject, or abstain. The function then updates the respective vote count within the storage of the proposal.
function executeProposal(uint256 _index) public {
Proposal storage proposal = proposals[_index];
require(block.timestamp > proposal.deadline, "ACTIVE_PROPOSAL");
if (proposal.accept >= proposal.reject) {
if (proposal.accept >= proposal.abstain) {
emit winner(_index, proposal.title, Vote.accept);
} else {
emit winner(_index, proposal.title, Vote.abstain);
}
}
else {
if (proposal.reject >= proposal.abstain){
emit winner(_index, proposal.title, Vote.reject);
} else{
emit winner(_index, proposal.title, Vote.abstain);
}
}
}
}
The executeProposal
function allows the smart contract to execute a proposal after its deadline has passed. Once the winning vote is determined, the function emits an event winner with the proposal’s index, title, and the winning vote as parameters.
Now, your final code should be looking like this
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract DAO {
IERC20 public token;
uint256 public proposalIndex;
event winner(uint256 _index, bytes32 proposal, Vote winningVote);
mapping(uint256 => Proposal) public proposals;
constructor(address _token) {
token = IERC20(_token);
}
struct Proposal {
uint256 accept;
uint256 reject;
uint256 abstain;
uint256 deadline;
bytes32 title;
mapping(address => bool) voted;
}
enum Vote {
accept,
reject,
abstain
}
modifier onlyTokenHolders() {
require(
token.balanceOf(msg.sender) > 0,
"NOT_A_TOKEN_HOLDER"
);
_;
}
function createProposal(bytes32 _proposal) public onlyTokenHolders returns(uint256) {
Proposal storage proposal = proposals[proposalIndex];
proposal.title = _proposal;
proposal.deadline = block.timestamp + 1 days;
proposalIndex++;
return proposalIndex - 1;
}
function voteOnProposal(uint256 _index, Vote _vote) public onlyTokenHolders {
Proposal storage proposal = proposals[_index];
require (proposal.deadline > block.timestamp, "Voting has ended");
require(proposal.voted[msg.sender] == false, "you have voted");
proposal.voted[msg.sender] = true;
if (_vote == Vote.accept) {
proposal.accept += token.balanceOf(msg.sender);
} else if (_vote == Vote.reject) {
proposal.reject += token.balanceOf(msg.sender);
} else if (_vote == Vote.abstain) {
proposal.abstain += token.balanceOf(msg.sender);
}
}
function executeProposal(uint256 _index) public {
Proposal storage proposal = proposals[_index];
require(block.timestamp > proposal.deadline, "ACTIVE_PROPOSAL");
if (proposal.accept >= proposal.reject) {
if (proposal.accept >= proposal.abstain) {
emit winner(_index, proposal.title, Vote.accept);
} else {
emit winner(_index, proposal.title, Vote.abstain);
}
}
else {
if (proposal.reject >= proposal.abstain){
emit winner(_index, proposal.title, Vote.reject);
} else{
emit winner(_index, proposal.title, Vote.abstain);
}
}
}
}
Deployment
To deploy our smart contract, we need to download the celo extension wallet on your web browser.
Next, we need to fund our wallet by requesting tokens on celo alfajores faucet
You can now fund your wallet and deploy your contract using the celo plugin in the Remix IDE.
Conclusion
In this tutorial, we have created a decentralized autonomous organization (DAO) contract on the Celo blockchain. The DAO allows token holders to create proposals and vote on them. After a specified deadline, the winning vote is determined based on the highest number of accepted votes.
Author
Kyrian Chigozie is a Technical Writer with a passion for web3 technology. Connect with me on Twitter.