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)