πŸ—³οΈ Day 28 of #30DaysOfSolidity β€” Build a DAO Voting System (Decentralized Governance)



This content originally appeared on DEV Community and was authored by Saurav Kumar

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

  1. Admin adds members who can vote.
  2. Members create proposals describing actions the DAO should take.
  3. Voting starts immediately after proposal creation.
  4. Members vote For or Against the proposal.
  5. 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.


This content originally appeared on DEV Community and was authored by Saurav Kumar