In today’s challenge, we’ll build a DAO Voting System — the backbone of Decentralized Governance.
It’s like a digital democracy where members can propose, vote, and execute decisions — all powered by smart contracts.
Let’s dive into how DAOs (Decentralized Autonomous Organizations) function on-chain and build our own using Solidity + Foundry.
💡 What is a DAO?
A DAO (Decentralized Autonomous Organization) is a community-led entity with no central authority.
Every decision — like protocol upgrades or treasury spending — is made through proposals and votes recorded on-chain.
Our DAO will:
- Allow members to create proposals.
- Let members vote for or against proposals.
- Automatically execute successful proposals.
🧱 Project Structure
Here’s the layout for our DAO project:
day-28-solidity/
├─ src/
│ └─ SimpleDAO.sol
├─ test/
│ └─ SimpleDAO.t.sol
├─ script/
│ └─ deploy.s.sol
├─ foundry.toml
└─ README.md
⚙️ Smart Contract — SimpleDAO.sol
Below is the full Solidity code implementing our DAO logic 👇
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
/// @title SimpleDAO - member-based governance for proposals & voting
/// @notice Demonstrates decentralized decision-making and on-chain governance.
contract SimpleDAO {
event MemberAdded(address indexed member);
event MemberRemoved(address indexed member);
event ProposalCreated(uint256 indexed id, address indexed proposer, uint256 startTime, uint256 endTime, string description);
event VoteCast(address indexed voter, uint256 indexed proposalId, bool support);
event ProposalExecuted(uint256 indexed id);
struct Proposal {
uint256 id;
address proposer;
address[] targets;
uint256[] values;
bytes[] calldatas;
string description;
uint256 startTime;
uint256 endTime;
uint256 forVotes;
uint256 againstVotes;
bool executed;
bool canceled;
}
mapping(address => bool) public isMember;
uint256 public memberCount;
mapping(uint256 => Proposal) public proposals;
uint256 public proposalCount;
mapping(uint256 => mapping(address => bool)) public hasVoted;
uint256 public votingPeriod;
uint256 public quorum;
address public admin;
modifier onlyAdmin() {
require(msg.sender == admin, "Only admin");
_;
}
modifier onlyMember() {
require(isMember[msg.sender], "Not a member");
_;
}
constructor(uint256 _votingPeriodSeconds, uint256 _quorum) {
votingPeriod = _votingPeriodSeconds;
quorum = _quorum;
admin = msg.sender;
}
function addMember(address _member) external onlyAdmin {
require(!isMember[_member], "Already member");
isMember[_member] = true;
memberCount++;
emit MemberAdded(_member);
}
function removeMember(address _member) external onlyAdmin {
require(isMember[_member], "Not member");
isMember[_member] = false;
memberCount--;
emit MemberRemoved(_member);
}
function propose(
address[] calldata targets,
uint256[] calldata values,
bytes[] calldata calldatas,
string calldata description
) external onlyMember returns (uint256) {
require(targets.length > 0, "Empty proposal");
proposalCount++;
uint256 id = proposalCount;
proposals[id] = Proposal({
id: id,
proposer: msg.sender,
targets: targets,
values: values,
calldatas: calldatas,
description: description,
startTime: block.timestamp,
endTime: block.timestamp + votingPeriod,
forVotes: 0,
againstVotes: 0,
executed: false,
canceled: false
});
emit ProposalCreated(id, msg.sender, block.timestamp, block.timestamp + votingPeriod, description);
return id;
}
function vote(uint256 proposalId, bool support) external onlyMember {
Proposal storage p = proposals[proposalId];
require(block.timestamp <= p.endTime, "Voting ended");
require(!hasVoted[proposalId][msg.sender], "Already voted");
hasVoted[proposalId][msg.sender] = true;
if (support) p.forVotes++;
else p.againstVotes++;
emit VoteCast(msg.sender, proposalId, support);
}
function execute(uint256 proposalId) external {
Proposal storage p = proposals[proposalId];
require(block.timestamp > p.endTime, "Voting not ended");
require(!p.executed, "Already executed");
require(p.forVotes >= quorum && p.forVotes > p.againstVotes, "Proposal failed");
p.executed = true;
for (uint256 i = 0; i < p.targets.length; i++) {
(bool success, ) = p.targets[i].call{value: p.values[i]}(p.calldatas[i]);
require(success, "Execution failed");
}
emit ProposalExecuted(proposalId);
}
receive() external payable {}
}
🧪 Testing the DAO (Foundry)
Testing ensures our proposal → vote → execute flow works perfectly.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import "forge-std/Test.sol";
import "../src/SimpleDAO.sol";
contract DummyTarget {
event Done(address sender);
function doSomething() external {
emit Done(msg.sender);
}
}
contract SimpleDAOTest is Test {
SimpleDAO dao;
DummyTarget target;
address alice = address(0xA11CE);
address bob = address(0xB0B);
function setUp() public {
dao = new SimpleDAO(1 hours, 2);
dao.addMember(alice);
dao.addMember(bob);
target = new DummyTarget();
}
function testProposalLifecycle() public {
vm.prank(alice);
address ;
targets[0] = address(target);
uint256 ;
values[0] = 0;
bytes ;
calldatas[0] = abi.encodeWithSelector(DummyTarget.doSomething.selector);
vm.prank(alice);
uint256 id = dao.propose(targets, values, calldatas, "Trigger doSomething()");
vm.warp(block.timestamp + 10);
vm.prank(alice);
dao.vote(id, true);
vm.prank(bob);
dao.vote(id, true);
vm.warp(block.timestamp + 1 hours + 1);
dao.execute(id);
}
}
✅ Run test:
forge test -vv
🧭 How It Works
- Admin adds members who can vote.
- Members create proposals describing actions the DAO should take.
- Voting starts immediately after proposal creation.
- Members vote For or Against the proposal.
- Once the voting period ends, if quorum is met and “for” votes win, the proposal executes automatically.
🧩 Example Scenario
Imagine a community treasury DAO with 10 members:
- A proposal is created to donate 2 ETH to an education project.
- Members vote.
- If majority agrees and quorum is reached, funds are sent automatically!
🛡️ Security Notes
- Only members can propose or vote.
- Quorum and majority ensure fairness.
- Execution is atomic — if any call fails, the entire transaction reverts.
- This version uses admin control for simplicity, but production DAOs should use token-based voting and timelocks.
⚡ Try It Yourself
Deploy it locally using Foundry:
forge create src/SimpleDAO.sol:SimpleDAO --rpc-url <RPC_URL> --private-key <PRIVATE_KEY> --constructor-args 3600 2
Then use Cast to call methods:
cast send <DAO_ADDRESS> "addMember(address)" <YOUR_MEMBER_ADDRESS> --private-key <PRIVATE_KEY>
🚀 Future Enhancements
You can level up this DAO with:
- 🪙 ERC20-based token voting (snapshot)
- ⏱️ Timelocks for proposal execution delay
- 🧾 Proposal cancellation / expiry
- 🗳️ Off-chain voting via signatures (gasless)
🎯 Summary
In this project, you learned how to:
- Create and manage proposals
- Implement on-chain voting logic
- Execute decentralized governance decisions
This forms the core foundation of DAOs — transparent, autonomous, and democratic organizations on blockchain.
💬 Final Thoughts
DAOs are the future of collective decision-making.
By mastering these core governance concepts, you can build your own decentralized organizations that run purely by community consensus.
Top comments (0)