DEV Community

Cover image for 🗳️ Day 28 of #30DaysOfSolidity — Build a DAO Voting System (Decentralized Governance)
Saurav Kumar
Saurav Kumar

Posted on

🗳️ Day 28 of #30DaysOfSolidity — Build a DAO Voting System (Decentralized Governance)

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
Enter fullscreen mode Exit fullscreen mode

⚙️ 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 {}
}
Enter fullscreen mode Exit fullscreen mode

🧪 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);
    }
}
Enter fullscreen mode Exit fullscreen mode

✅ Run test:

forge test -vv
Enter fullscreen mode Exit fullscreen mode

🧭 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
Enter fullscreen mode Exit fullscreen mode

Then use Cast to call methods:

cast send <DAO_ADDRESS> "addMember(address)" <YOUR_MEMBER_ADDRESS> --private-key <PRIVATE_KEY>
Enter fullscreen mode Exit fullscreen mode

🚀 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)