This tutorial provides a step-by-step method for creating and implementing a Decentralised Autonomous Organisation (DAO) on the zkSync era blockchain, a layer 2 scaling solution for Ethereum using zksync-cli. The 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: Writing Tests
- Section 8: Compile and Deploy
- Section 9 Frontend Integration with Next.js
zkSync is a Layer 2 scaling solution for Ethereum which is designed to enhance the scalability and increase the transaction throughput of the Ethereum network by reducing transaction costs while ensuring security and decentralization.
A Decentralized Autonomous Organization (DAO) is known as an organization on the blockchain. It is a decentralized organization built on the blockchain, which is secured, transparent, controlled by organization members, and not influenced by any central authority. They are programmed to automate decisions and facilitate cryptocurrency transactions.
- Basic understanding of Solidity.
- Visual studio code (VS code) or remix ide.
- Faucet: Follow this guide to obtain zkSync faucet.
- Node js is installed on your machine.
We can leverage on zksync-cli to kickstart our project which offers templates for frontend development, smart contracts, and scripting for zkSync, enabling rapid deployment and development.
Navigate to the terminal and run the command below to get started with our project.
npx zksync-cli create dao-tutorial --template hardhat_solidity
dao-tutorial
represents our folder's name where all our project files and dependencies will reside.
--template
refers to the ethereum framework we want to use. In our case, hardhat
for solidity
.
Follow the prompts in the terminal for your project setup.
After successful installations, you can remove all generated files that we don't need in this project by running the below command :
cd dao-tutorial && rm -rf ./contracts/* && rm -rf ./deploy/erc20 && rm -rf ./deploy/nft && rm -rf ./test/*
Lastly, open your contracts
folder and create a new file DAO.sol
inside it and paste the code below in the file.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.2;
import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
contract DAO 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/security/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;
}
Next, let's proceed to writning tests for the DAO contract.
First, let's import the packages needed to run the tests.
import { expect,assert } from 'chai';
import { Contract, Wallet } from "zksync-ethers";
import { getWallet, deployContract, LOCAL_RICH_WALLETS } from '../deploy/utils';
import * as ethers from "ethers";
Next, create a describe
function DAO
which holds all the methods and variables needed to run the tests.
describe("DAO",()=>{
})
Inside, the describe
function, declare variables to hold wallets and initialize them in the beforeEach
method for
deployment.
let DAO : Contract
let stakeholder : Wallet
let contributor : Wallet
let deployer : Wallet
let beneficiary : Wallet
beforeEach(async ()=>{
stakeholder = getWallet(LOCAL_RICH_WALLETS[0].privateKey);
contributor = getWallet(LOCAL_RICH_WALLETS[1].privateKey);
deployer = getWallet(LOCAL_RICH_WALLETS[2].privateKey);
beneficiary = getWallet(LOCAL_RICH_WALLETS[3].privateKey);
DAO = await deployContract("DAO", [], { wallet: deployer, silent: true });
})
Next, let's begin writing tests. The tests are organized into several categories:
- Stakeholders and Contributors: Tests related to contributions and balances of stakeholders and contributors.
- Proposals: Tests for creating and retrieving proposals.
- Voting: Tests for performing upvotes and downvotes, and retrieving proposal votes.
- Payments: Test for paying the beneficiary.
Go ahead and create a describe function stakeholders and contributor
and write the below related tests inside it.
describe("stakeholders and contributors", ()=>{
})
-
Stakeholder Contributes and Retrieves Balance:
Tests if a stakeholder can contribute to the DAO and retrieve their balance.
it("stakeholder contributes and retrieves balance", async () => { let price = ethers.parseEther('1'); await (DAO.connect(stakeholder) as Contract).contribute({ value: price }); let balance = await (DAO.connect(stakeholder) as Contract).getStakeholdersBalances(); assert.equal(balance, price.toString()); });
-
Contributor Contributes and Retrieves Balance:
Tests if a contributor can contribute to the DAO and retrieve their balance.
it("collaborator contributes and retrieves balance", async () => { let price = ethers.parseEther('0.05'); await (DAO.connect(contributor) as Contract).contribute({ value: price }); let balance = await (DAO.connect(contributor) as Contract).getContributorsBalance(); assert.equal(balance, price.toString()); });
-
Check Stakeholder Status:
Tests if a stakeholder status can be checked.
it("checks stakeholder status", async () => { let price = ethers.parseEther('1'); await (DAO.connect(stakeholder) as Contract).contribute({ value: price }); let stakeholderStatus = await (DAO.connect(stakeholder) as Contract).stakeholderStatus(); assert.equal(stakeholderStatus, true); });
-
Check Contributor Status:
Tests if a contributor status can be checked.
it("checks contributors status", async () => { let price = ethers.parseEther('0.05'); await (DAO.connect(contributor) as Contract).contribute({ value: price }); let contributorStatus = await (DAO.connect(contributor) as Contract).isContributor(); assert.equal(contributorStatus, true); });
Go ahead and create a describe function proposal
and write the below related tests inside it.
describe("proposal", ()=>{
})
-
Create Proposal:
Tests if a proposal can be created.
it("creates proposal", async () => { let amount = ethers.parseEther('1'); await (DAO.connect(stakeholder) as Contract).contribute({ value: amount }); let proposalTx = await (DAO.connect(stakeholder) as Contract).createProposal('title', 'desc', beneficiary.address, amount); const receipt = await proposalTx.wait(); const event = receipt.logs.find((log) => { const parsedLog = DAO.interface.parseLog(log); return parsedLog?.name === 'ProposalAction'; }); assert.equal(event.args[2], 'Proposal Raised'); assert.equal(event.args[3], beneficiary.address); assert.equal(event.args[4], amount.toString()); });
-
Retrieve Proposal:
Tests if a proposal can be retrieved.
it("retrieves proposal", async () => { let amount = ethers.parseEther('1'); await (DAO.connect(stakeholder) as Contract).contribute({ value: amount }); await (DAO.connect(stakeholder) as Contract).createProposal('title', 'desc', beneficiary.address, amount); let firstProposal = await DAO.getProposals(0); expect(firstProposal.id.toString()).to.equal('0'); expect(firstProposal.title).to.equal('title'); expect(firstProposal.description).to.equal('desc'); expect(firstProposal.beneficiary).to.equal(beneficiary.address); expect(firstProposal.amount.toString()).to.equal(amount.toString()); });
Go ahead and create a describe function voting and payment
and write the below related tests inside it.
describe("voting and payment", ()=>{
})
-
Perform Upvote:
Tests if a stakeholder can upvote a proposal.
it("performs upvote", async () => { let price = ethers.parseEther('0.5'); let amount = ethers.parseEther('4'); // Stakeholder contributes to the DAO await (DAO.connect(stakeholder) as Contract).contribute({ value: price }); // Stakeholder creates a proposal await (DAO.connect(stakeholder) as Contract).createProposal('title', 'desc', beneficiary.address, amount); // Stakeholder performs an upvote on the proposal let voteTx = await (DAO.connect(stakeholder) as Contract).performVote(0, true); // Wait for the transaction to be mined and get the receipt const receipt = await voteTx.wait(); // Find the 'VoteAction' event in the logs const event = receipt.logs.find((log) => { const parsedLog = DAO.interface.parseLog(log); return parsedLog?.name === 'VoteAction'; }); // Assertions to check the event details expect(event.args[7]).to.equal(true); expect(event.args[4].toString()).to.equal(amount.toString()); expect(event.args[3]).to.equal(beneficiary.address); });
-
Perform Downvote:
Tests if a stakeholder can downvote a proposal.
it("performs downvote", async () => { let price = ethers.parseEther('0.5'); let amount = ethers.parseEther('4'); // Stakeholder contributes to the DAO await (DAO.connect(stakeholder) as Contract).contribute({ value: price }); // Stakeholder creates a proposal await (DAO.connect(stakeholder) as Contract).createProposal('title', 'desc', beneficiary.address, amount); // Stakeholder performs a downvote on the proposal let voteTx = await (DAO.connect(stakeholder) as Contract).performVote(0, false); // Wait for the transaction to be mined and get the receipt const receipt = await voteTx.wait(); // Find the 'VoteAction' event in the logs const event = receipt.logs.find((log) => { const parsedLog = DAO.interface.parseLog(log); return parsedLog?.name === 'VoteAction'; }); expect(event.args[7]).to.equal(false); expect(event.args[4].toString()).to.equal(amount.toString()); expect(event.args[3]).to.equal(beneficiary.address); });
-
Retrieve Proposal Vote:
Tests if a vote on a proposal can be retrieved.
it("retrieves proposal vote", async () => { let price = ethers.parseEther('0.5'); let amount = ethers.parseEther('4'); await (DAO.connect(stakeholder) as Contract).contribute({ value: price }); await (DAO.connect(stakeholder) as Contract).createProposal('title', 'desc', beneficiary.address, amount); await (DAO.connect(stakeholder) as Contract).performVote(0, true); let vote = await DAO.getProposalVote(0); assert.equal(vote[0].voter, stakeholder.address); });
it("pays beneficiary", async () => { let previousBalance, currentBalance; let price = ethers.parseEther('0.5'); let amount = ethers.parseEther('0.02'); await (DAO.connect(deployer) as Contract).contribute({ value: price }); await (DAO.connect(deployer) as Contract).createProposal('title', 'desc', beneficiary.address, amount); await (DAO.connect(deployer) as Contract).performVote(0, true); previousBalance = await DAO.getTotalBalance(); const processPaymentTx = await (DAO.connect(deployer) as Contract).payBeneficiary(0); const receipt = await processPaymentTx.wait(); const event = receipt.logs.find((log) => { const parsedLog = DAO.interface.parseLog(log); return parsedLog?.name === 'ProposalAction'; }); assert.equal(event.args[3], beneficiary.address); currentBalance = await DAO.getTotalBalance(); assert.equal(previousBalance.toString(), price.toString()); assert.equal(currentBalance.toString(), ethers.parseEther('0.48').toString()); });
Finally, let's run the tests by running the below commands in the terminal:
npm run test
If the tests are successful, you should see a similar result to the one below where all test cases passed.
Run npm run compile
to compile your smart contract. If it is compiled successfully, your terminal should produce a result like below
Now, let's go ahead and deploy our smart contract. Two things should be in place before you run your deployment script.
The first thing is .env
, your private key should be already set and the second thing is that your account should hold some faucets to deploy to the zkSync sepolia testnet.
Next, replace deploy.ts
in your deploy
folder with the following code.
import { deployContract } from "./utils";
// An example of a basic deploy script
// It will deploy a Greeter contract to selected network
// as well as verify it on Block Explorer if possible for the network
export default async function () {
const contractArtifactName = "DAO";
const constructorArguments = [];
await deployContract(contractArtifactName, constructorArguments);
}
Finally, run npm run deploy
to deploy your contract. You should see a similar result below if it is deployed successfully.
This section provides a step-by-step guide to integrate the DAO contract with a Next.js frontend.
-
Create a new Next.js project:
npx create-next-app@latest dao-frontend cd dao-frontend
-
Install necessary dependencies:
npm install ethers
-
Create a new file
utils/dao.js
to set up the DAO contract interaction :import { ethers } from 'ethers'; import DAO_ABI from './DAO_ABI.json'; // Import the ABI of your DAO contract const DAO_ADDRESS = 'YOUR_DAO_CONTRACT_ADDRESS'; // Replace with your DAO contract address export const getDAOContract = () => { if (typeof window.ethereum !== 'undefined') { const provider = new ethers.BrowserProvider(window.ethereum); const signer = provider.getSigner(); const contract = new ethers.Contract(DAO_ADDRESS, DAO_ABI, signer); return contract; } else { console.error('Ethereum wallet is not available'); return null; } };
-
Create a new file
pages/index.js
for the main interface:"use client"; import { useEffect, useState } from 'react'; import { ethers } from 'ethers'; import { getDAOContract } from '../utils/dao'; import 'bootstrap/dist/css/bootstrap.min.css'; const Home = () => { const [stakeholder, setStakeholder] = useState<string | null>(null); const [balance, setBalance] = useState<string>('0'); const [daoBalance, setDaoBalance] = useState<string>('0'); const [stakeholderStatus, setStakeholderStatus] = useState(false); const [contributorStatus, setContributorStatus] = useState(false); const [contributeAmount, setContributeAmount] = useState<string>('0'); useEffect(() => { const loadBlockchainData = async () => { try { const daoContract = getDAOContract(); if (daoContract && typeof window !== 'undefined') { const accounts = await (window as any).ethereum.request({ method: 'eth_requestAccounts' }); const balance = await daoContract.getStakeholdersBalances(); const daoTotalBalance = await daoContract.getTotalBalance(); const isStakeholder = await daoContract.stakeholderStatus(); const isContributor = await daoContract.isContributor(); setStakeholder(accounts[0]); setBalance(ethers.formatEther(balance)); setDaoBalance(ethers.formatEther(daoTotalBalance)); setStakeholderStatus(isStakeholder); setContributorStatus(isContributor); } } catch (error) { console.error("Error loading blockchain data", error); } }; loadBlockchainData(); }, []); const handleContribute = async () => { try { const daoContract = getDAOContract(); if (daoContract && contributeAmount) { const tx = await daoContract.contribute({ value: ethers.parseEther(contributeAmount) }); await tx.wait(); const balance = await daoContract.getStakeholdersBalances(); setBalance(ethers.formatEther(balance)); } } catch (error) { console.error("Error contributing", error); } }; return ( <div className="bg-light min-vh-100 d-flex flex-column align-items-center justify-content-center"> <h1 className="mb-5">DAO Interface</h1> <div className="card shadow p-4" style={{ width: '400px' }}> <div className="card-body"> <h5 className="card-title">Stakeholder</h5> <p className="card-text">{stakeholder}</p> <h5 className="card-title">Balance</h5> <p className="card-text">{balance} ETH</p> <h5 className="card-title">Total DAO Balance</h5> <p className="card-text">{daoBalance} ETH</p> <h5 className="card-title">Status</h5> <p className="card-text"> {stakeholderStatus ? 'Stakeholder' : contributorStatus ? 'Contributor' : 'New User'} </p> <div className="input-group mb-3"> <input type="number" className="form-control" placeholder="Contribute min 0.1ETH to be a stakeholder" value={contributeAmount} onChange={(e) => setContributeAmount(e.target.value)} /> <button className="btn btn-primary" onClick={handleContribute}>Contribute</button> </div> </div> </div> </div> ); }; export default Home;
-
Create the
DAO_ABI.json
file in theutils
directory, and paste the ABI of your DAO contract into it.
-
Start the Next.js development server:
npm run dev
-
Open your browser and navigate to
http://localhost:3000
to interact with the DAO contract through the frontend interface.
If everything works well, you should see a similar page like below :
Congratulations! You have made it to the end of the DAO tutorial, Smart contract, Testing, Deployment and Frontend Integration.