This tutorial provides a step-by-step method for creating and implementing a Decentralised Autonomous Organisation (DAO) on the Celo blockchain using hardhat deploy. The provided Solidity smart contract utilizes OpenZeppelin components for improved functionality and security.
- Section 1: Recognising the Fundamentals
- Section 2: Smart Contract Development
- Section 3: Code Explanation of the Smart Contracts
- Section 4: Involvement of Stakeholders
- Section 5: Proposal Execution and Payments
- Section 6: Stakeholders and Contributors
- Section 7: Deploying the DAO on Celo using hardhat deploy
Decentralized Autonomous Organisations (DAOs) revolutionize community-driven decision-making.
To begin, understand the basic framework of the Solidity-written DAO smart contract. The code incorporates the AccessControl and ReentrancyGuard libraries from OpenZeppelin, adding access control methods and defense against reentrancy attacks.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
contract CeloDao is AccessControl,ReentrancyGuard {
uint256 totalProposals;
uint256 balance;
address deployer;
uint256 immutable STAKEHOLDER_MIN_CONTRIBUTION = 0.1 ether;
uint256 immutable MIN_VOTE_PERIOD = 5 minutes;
bytes32 private immutable COLLABORATOR_ROLE = keccak256("collaborator");
bytes32 private immutable STAKEHOLDER_ROLE = keccak256("stakeholder");
mapping(uint256 => Proposals) private raisedProposals;
mapping(address => uint256[]) private stakeholderVotes;
mapping(uint256 => Voted[]) private votedOn;
mapping(address => uint256) private contributors;
mapping(address => uint256) private stakeholders;
struct Proposals {
uint256 id;
uint256 amount;
uint256 upVote;
uint256 downVotes;
uint256 duration;
string title;
string description;
bool paid;
bool passed;
address payable beneficiary;
address propoper;
address executor;
}
struct Voted {
address voter;
uint256 timestamp;
bool chosen;
}
modifier stakeholderOnly(string memory message) {
require(hasRole(STAKEHOLDER_ROLE,msg.sender),message);
_;
}
modifier contributorOnly(string memory message){
require(hasRole(COLLABORATOR_ROLE,msg.sender),message);
_;
}
modifier onlyDeployer(string memory message) {
require(msg.sender == deployer,message);
_;
}
event ProposalAction(
address indexed creator,
bytes32 role,
string message,
address indexed beneficiary,
uint256 amount
);
event VoteAction(
address indexed creator,
bytes32 role,
string message,
address indexed beneficiary,
uint256 amount,
uint256 upVote,
uint256 downVotes,
bool chosen
);
constructor(){
deployer = msg.sender;
}
// proposal creation
function createProposal (
string calldata title,
string calldata description,
address beneficiary,
uint256 amount
)external stakeholderOnly("Only stakeholders are allowed to create Proposals") returns(Proposals memory){
uint256 currentID = totalProposals++;
Proposals storage StakeholderProposal = raisedProposals[currentID];
StakeholderProposal.id = currentID;
StakeholderProposal.amount = amount;
StakeholderProposal.title = title;
StakeholderProposal.description = description;
StakeholderProposal.beneficiary = payable(beneficiary);
StakeholderProposal.duration = block.timestamp + MIN_VOTE_PERIOD;
emit ProposalAction(
msg.sender,
STAKEHOLDER_ROLE,
'Proposal Raised',
beneficiary,
amount
);
return StakeholderProposal;
}
// voting
function performVote(uint256 proposalId,bool chosen) external
stakeholderOnly("Only stakeholders can perform voting")
returns(Voted memory)
{
Proposals storage StakeholderProposal = raisedProposals[proposalId];
handleVoting(StakeholderProposal);
if(chosen) StakeholderProposal.upVote++;
else StakeholderProposal.downVotes++;
stakeholderVotes[msg.sender].push(
StakeholderProposal.id
);
votedOn[StakeholderProposal.id].push(
Voted(
msg.sender,
block.timestamp,
chosen
)
);
emit VoteAction(
msg.sender,
STAKEHOLDER_ROLE,
"PROPOSAL VOTE",
StakeholderProposal.beneficiary,
StakeholderProposal.amount,
StakeholderProposal.upVote,
StakeholderProposal.downVotes,
chosen
);
return Voted(
msg.sender,
block.timestamp,
chosen
);
}
// handling vote
function handleVoting(Proposals storage proposal) private {
if (proposal.passed || proposal.duration <= block.timestamp) {
proposal.passed = true;
revert("Time has already passed");
}
uint256[] memory tempVotes = stakeholderVotes[msg.sender];
for (uint256 vote = 0; vote < tempVotes.length; vote++) {
if (proposal.id == tempVotes[vote])
revert("double voting is not allowed");
}
}
// pay beneficiary
function payBeneficiary(uint proposalId) external
stakeholderOnly("Only stakeholders can make payment") onlyDeployer("Only deployer can make payment") nonReentrant() returns(uint256){
Proposals storage stakeholderProposal = raisedProposals[proposalId];
require(balance >= stakeholderProposal.amount, "insufficient fund");
if(stakeholderProposal.paid == true) revert("payment already made");
if(stakeholderProposal.upVote <= stakeholderProposal.downVotes) revert("insufficient votes");
pay(stakeholderProposal.amount,stakeholderProposal.beneficiary);
stakeholderProposal.paid = true;
stakeholderProposal.executor = msg.sender;
balance -= stakeholderProposal.amount;
emit ProposalAction(
msg.sender,
STAKEHOLDER_ROLE,
"PAYMENT SUCCESSFULLY MADE!",
stakeholderProposal.beneficiary,
stakeholderProposal.amount
);
return balance;
}
// paymment functionality
function pay(uint256 amount,address to) internal returns(bool){
(bool success,) = payable(to).call{value : amount}("");
require(success, "payment failed");
return true;
}
// contribution functionality
function contribute() payable external returns(uint256){
require(msg.value > 0 ether, "invalid amount");
if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) {
uint256 totalContributions = contributors[msg.sender] + msg.value;
if (totalContributions >= STAKEHOLDER_MIN_CONTRIBUTION) {
stakeholders[msg.sender] = msg.value;
contributors[msg.sender] += msg.value;
_grantRole(STAKEHOLDER_ROLE,msg.sender);
_grantRole(COLLABORATOR_ROLE, msg.sender);
}
else {
contributors[msg.sender] += msg.value;
_grantRole(COLLABORATOR_ROLE,msg.sender);
}
}
else{
stakeholders[msg.sender] += msg.value;
contributors[msg.sender] += msg.value;
}
balance += msg.value;
emit ProposalAction(
msg.sender,
STAKEHOLDER_ROLE,
"CONTRIBUTION SUCCESSFULLY RECEIVED!",
address(this),
msg.value
);
return balance;
}
// get single proposal
function getProposals(uint256 proposalID) external view returns(Proposals memory) {
return raisedProposals[proposalID];
}
// get all proposals
function getAllProposals() external view returns(Proposals[] memory props){
props = new Proposals[](totalProposals);
for (uint i = 0; i < totalProposals; i++) {
props[i] = raisedProposals[i];
}
}
// get a specific proposal votes
function getProposalVote(uint256 proposalID) external view returns(Voted[] memory){
return votedOn[proposalID];
}
// get stakeholders votes
function getStakeholdersVotes() stakeholderOnly("Unauthorized") external view returns(uint256[] memory){
return stakeholderVotes[msg.sender];
}
// get stakeholders balances
function getStakeholdersBalances() stakeholderOnly("unauthorized") external view returns(uint256){
return stakeholders[msg.sender];
}
// get total balances
function getTotalBalance() external view returns(uint256){
return balance;
}
// check if stakeholder
function stakeholderStatus() external view returns(bool){
return stakeholders[msg.sender] > 0;
}
// check if contributor
function isContributor() external view returns(bool){
return contributors[msg.sender] > 0;
}
// check contributors balance
function getContributorsBalance() contributorOnly("unathorized") external view returns(uint256){
return contributors[msg.sender];
}
function getDeployer()external view returns(address){
return deployer;
}
}
Utilize OpenZeppelin's AccessControl package, providing role-based access control for secure interactions with collaborators and stakeholders.
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
this function allows stakeholders to create proposals by providing essential details such as title
description
, beneficiary
, and amount
. The function ensures that only stakeholders
can initiate proposals, and it emits an event to notify external applications about the proposal creation.
function createProposal (
string calldata title,
string calldata description,
address beneficiary,
uint256 amount
)external stakeholderOnly("Only stakeholders are allowed to create Proposals") returns(Proposals memory){
uint256 currentID = totalProposals++;
Proposals storage StakeholderProposal = raisedProposals[currentID];
StakeholderProposal.id = currentID;
StakeholderProposal.amount = amount;
StakeholderProposal.title = title;
StakeholderProposal.description = description;
StakeholderProposal.beneficiary = payable(beneficiary);
StakeholderProposal.duration = block.timestamp + MIN_VOTE_PERIOD;
emit ProposalAction(
msg.sender,
STAKEHOLDER_ROLE,
'Proposal Raised',
beneficiary,
amount
);
return StakeholderProposal;
}
this function allows contributors to send Ether to the contract. If the contributor is not a stakeholder, it checks whether their total contributions meet the minimum requirement. If so, the contributor becomes a stakeholder and collaborator; otherwise, they become a collaborator only.
function contribute() payable external returns(uint256){
require(msg.value > 0 ether, "invalid amount");
if (!hasRole(STAKEHOLDER_ROLE, msg.sender)) {
uint256 totalContributions = contributors[msg.sender] + msg.value;
if (totalContributions >= STAKEHOLDER_MIN_CONTRIBUTION) {
stakeholders[msg.sender] = msg.value;
contributors[msg.sender] += msg.value;
_grantRole(STAKEHOLDER_ROLE,msg.sender);
_grantRole(COLLABORATOR_ROLE, msg.sender);
}
else {
contributors[msg.sender] += msg.value;
_grantRole(COLLABORATOR_ROLE,msg.sender);
}
}
else{
stakeholders[msg.sender] += msg.value;
contributors[msg.sender] += msg.value;
}
balance += msg.value;
emit ProposalAction(
msg.sender,
STAKEHOLDER_ROLE,
"CONTRIBUTION SUCCESSFULLY RECEIVED!",
address(this),
msg.value
);
return balance;
}
this function facilitates the voting process for stakeholders, updating proposal details, recording votes, and emitting an event to notify external applications about the voting action.
function performVote(uint256 proposalId,bool chosen) external
stakeholderOnly("Only stakeholders can perform voting")
returns(Voted memory)
{
Proposals storage StakeholderProposal = raisedProposals[proposalId];
handleVoting(StakeholderProposal);
if(chosen) StakeholderProposal.upVote++;
else StakeholderProposal.downVotes++;
stakeholderVotes[msg.sender].push(
StakeholderProposal.id
);
votedOn[StakeholderProposal.id].push(
Voted(
msg.sender,
block.timestamp,
chosen
)
);
emit VoteAction(
msg.sender,
STAKEHOLDER_ROLE,
"PROPOSAL VOTE",
StakeholderProposal.beneficiary,
StakeholderProposal.amount,
StakeholderProposal.upVote,
StakeholderProposal.downVotes,
chosen
);
return Voted(
msg.sender,
block.timestamp,
chosen
);
}
this function ensures the necessary conditions are met before making a payment to the beneficiary of a proposal. It records the payment details, updates the contract's balance, and emits an event to inform external applications about the successful payment action.
function payBeneficiary(uint proposalId) external
stakeholderOnly("Only stakeholders can make payment") onlyDeployer("Only deployer can make payment") nonReentrant() returns(uint256){
Proposals storage stakeholderProposal = raisedProposals[proposalId];
require(balance >= stakeholderProposal.amount, "insufficient fund");
if(stakeholderProposal.paid == true) revert("payment already made");
if(stakeholderProposal.upVote <= stakeholderProposal.downVotes) revert("insufficient votes");
pay(stakeholderProposal.amount,stakeholderProposal.beneficiary);
stakeholderProposal.paid = true;
stakeholderProposal.executor = msg.sender;
balance -= stakeholderProposal.amount;
emit ProposalAction(
msg.sender,
STAKEHOLDER_ROLE,
"PAYMENT SUCCESSFULLY MADE!",
stakeholderProposal.beneficiary,
stakeholderProposal.amount
);
return balance;
}
this function retrieves single proposal using proposalID
function getProposals(uint256 proposalID) external view returns(Proposals memory) {
return raisedProposals[proposalID];
}
this function retrieves all proposals
function getAllProposals() external view returns(Proposals[] memory props){
props = new Proposals[](totalProposals);
for (uint i = 0; i < totalProposals; i++) {
props[i] = raisedProposals[i];
}
}
this function retrieves proposal votes
function getProposalVote(uint256 proposalID) external view returns(Voted[] memory){
return votedOn[proposalID];
}
this function retrieves stakeholder votes
function getStakeholdersVotes() stakeholderOnly("Unauthorized") external view returns(uint256[] memory){
return stakeholderVotes[msg.sender];
}
this function retrieves stakeholder balance
function getStakeholdersBalances() stakeholderOnly("unauthorized") external view returns(uint256){
return stakeholders[msg.sender];
}
this function retrieves the balance of the DAO
function getTotalBalance() external view returns(uint256){
return balance;
}
this function checks stakeholder status
function stakeholderStatus() external view returns(bool){
return stakeholders[msg.sender] > 0;
}
this function checks the contributor status
function isContributor() external view returns(bool){
return contributors[msg.sender] > 0;
}
this function retrieves the contributor's balance
function getContributorsBalance() contributorOnly("unathorized") external view returns(uint256){
return contributors[msg.sender];
}
this function returns the deployer address
function getDeployer()external view returns(address){
return deployer;
}
Open your terminal and run the following commands
Create folder mkdir celo-tut-dao
Enter the folder cd celo-tut-dao
Initialize a node js project npm init -y
Install hardhat npm install --save-dev hardhat
Initialize hardhat npx hardhat init
Select create a JavaScript project
install required dependencies npm install --save-dev "hardhat@^2.19.2" "dotenv" "@nomicfoundation/hardhat-toolbox@^4.0.0" "@openzeppelin/contracts" "hardhat-deploy"
replace your hardhat.config.js
with the following code
require("@nomicfoundation/hardhat-toolbox");
require("hardhat-deploy")
require("dotenv").config()
/** @type import('hardhat/config').HardhatUserConfig */
const PRIVATE_KEY = process.env.PRIVATE_KEY || "0x"
module.exports = {
solidity: "0.8.20",
networks: {
hardhat: {
chainId: 31337,
},
localhost: {
chainId: 31337,
},
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: [PRIVATE_KEY],
chainId: 44787
},
celo: {
url: "https://forno.celo.org",
accounts: [PRIVATE_KEY],
chainId: 42220
}
},
namedAccounts: {
deployer: {
default: 0, // here this will by default take the first account as deployer
1: 0, // similarly on mainnet it will take the first account as deployer. Note though that depending on how hardhat network are configured, the account 0 on one network can be different than on another
},
},
};
Replace Lock.sol
in the contract folder with Dao.sol
and the code.
Add your private key to the .env
file.
run npx hardhat compile
to compile the contract
create deploy
folder, add deploy.js
file to it and paste the following.
module.exports = async ({getNamedAccounts, deployments}) => {
const {deploy} = deployments;
const {deployer} = await getNamedAccounts();
await deploy('CeloDao', {
from: deployer,
args: [],
log: true,
});
};
module.exports.tags = ['CeloDao'];
run npx hardhat deploy --network alfajores
to deploy to alfajores testnet
run npx hardhat deploy --network celo
to deploy to celo mainnet
Congratulations! You have successfully deployed your DAO on the Celo blockchain using hardhat deploy
. Feel free to explore further and test the various functionalities of your DAO.