🎓 A1Learner
🔍
0 of 48 lessons completed

Solidity Project: DAO Voting

A DAO voting project is a contract where members propose ideas and vote on them as a group. A proposal passes when more members vote for it than against it.

A member proposal starts with zero votes

A member proposes an idea and it gets the next proposal id. Its votes for and against both start at zero.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleDAO {
    address public owner;
    mapping(address => bool) public members;
    uint256 public proposalCount;

    struct Proposal {
        string description;
        uint256 votesFor;
        uint256 votesAgainst;
        bool executed;
    }

    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    constructor() {
        owner = msg.sender;
        members[msg.sender] = true;
    }

    function addMember(address member) public {
        require(msg.sender == owner, "only owner adds members");
        members[member] = true;
    }

    function propose(string memory description) public returns (uint256) {
        require(members[msg.sender], "only members propose");
        uint256 id = proposalCount;
        proposals[id].description = description;
        proposalCount += 1;
        return id;
    }

    function vote(uint256 id, bool support) public {
        require(members[msg.sender], "only members vote");
        require(id < proposalCount, "no such proposal");
        require(!hasVoted[id][msg.sender], "already voted");
        hasVoted[id][msg.sender] = true;
        if (support) {
            proposals[id].votesFor += 1;
        } else {
            proposals[id].votesAgainst += 1;
        }
    }

    function execute(uint256 id) public {
        require(members[msg.sender], "only members execute");
        require(id < proposalCount, "no such proposal");
        require(!proposals[id].executed, "already executed");
        require(proposals[id].votesFor > proposals[id].votesAgainst, "needs more votes for");
        proposals[id].executed = true;
    }
}

A vote counts once per member

A member votes for a proposal and the votes for count rises to one. The same member voting again reverts.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleDAO {
    address public owner;
    mapping(address => bool) public members;
    uint256 public proposalCount;

    struct Proposal {
        string description;
        uint256 votesFor;
        uint256 votesAgainst;
        bool executed;
    }

    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    constructor() {
        owner = msg.sender;
        members[msg.sender] = true;
    }

    function addMember(address member) public {
        require(msg.sender == owner, "only owner adds members");
        members[member] = true;
    }

    function propose(string memory description) public returns (uint256) {
        require(members[msg.sender], "only members propose");
        uint256 id = proposalCount;
        proposals[id].description = description;
        proposalCount += 1;
        return id;
    }

    function vote(uint256 id, bool support) public {
        require(members[msg.sender], "only members vote");
        require(id < proposalCount, "no such proposal");
        require(!hasVoted[id][msg.sender], "already voted");
        hasVoted[id][msg.sender] = true;
        if (support) {
            proposals[id].votesFor += 1;
        } else {
            proposals[id].votesAgainst += 1;
        }
    }

    function execute(uint256 id) public {
        require(members[msg.sender], "only members execute");
        require(id < proposalCount, "no such proposal");
        require(!proposals[id].executed, "already executed");
        require(proposals[id].votesFor > proposals[id].votesAgainst, "needs more votes for");
        proposals[id].executed = true;
    }
}

A non-member vote reverts

Only members may vote on a proposal. A stranger voting on the same proposal reverts and the count stays put.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleDAO {
    address public owner;
    mapping(address => bool) public members;
    uint256 public proposalCount;

    struct Proposal {
        string description;
        uint256 votesFor;
        uint256 votesAgainst;
        bool executed;
    }

    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    constructor() {
        owner = msg.sender;
        members[msg.sender] = true;
    }

    function addMember(address member) public {
        require(msg.sender == owner, "only owner adds members");
        members[member] = true;
    }

    function propose(string memory description) public returns (uint256) {
        require(members[msg.sender], "only members propose");
        uint256 id = proposalCount;
        proposals[id].description = description;
        proposalCount += 1;
        return id;
    }

    function vote(uint256 id, bool support) public {
        require(members[msg.sender], "only members vote");
        require(id < proposalCount, "no such proposal");
        require(!hasVoted[id][msg.sender], "already voted");
        hasVoted[id][msg.sender] = true;
        if (support) {
            proposals[id].votesFor += 1;
        } else {
            proposals[id].votesAgainst += 1;
        }
    }

    function execute(uint256 id) public {
        require(members[msg.sender], "only members execute");
        require(id < proposalCount, "no such proposal");
        require(!proposals[id].executed, "already executed");
        require(proposals[id].votesFor > proposals[id].votesAgainst, "needs more votes for");
        proposals[id].executed = true;
    }
}

A winning proposal executes, a losing one cannot

One vote for and none against lets the proposal execute. A proposal with more votes against it cannot execute.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleDAO {
    address public owner;
    mapping(address => bool) public members;
    uint256 public proposalCount;

    struct Proposal {
        string description;
        uint256 votesFor;
        uint256 votesAgainst;
        bool executed;
    }

    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    constructor() {
        owner = msg.sender;
        members[msg.sender] = true;
    }

    function addMember(address member) public {
        require(msg.sender == owner, "only owner adds members");
        members[member] = true;
    }

    function propose(string memory description) public returns (uint256) {
        require(members[msg.sender], "only members propose");
        uint256 id = proposalCount;
        proposals[id].description = description;
        proposalCount += 1;
        return id;
    }

    function vote(uint256 id, bool support) public {
        require(members[msg.sender], "only members vote");
        require(id < proposalCount, "no such proposal");
        require(!hasVoted[id][msg.sender], "already voted");
        hasVoted[id][msg.sender] = true;
        if (support) {
            proposals[id].votesFor += 1;
        } else {
            proposals[id].votesAgainst += 1;
        }
    }

    function execute(uint256 id) public {
        require(members[msg.sender], "only members execute");
        require(id < proposalCount, "no such proposal");
        require(!proposals[id].executed, "already executed");
        require(proposals[id].votesFor > proposals[id].votesAgainst, "needs more votes for");
        proposals[id].executed = true;
    }
}

Example

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.30;

contract SimpleDAO {
    address public owner;
    mapping(address => bool) public members;
    uint256 public proposalCount;

    struct Proposal {
        string description;
        uint256 votesFor;
        uint256 votesAgainst;
        bool executed;
    }

    mapping(uint256 => Proposal) public proposals;
    mapping(uint256 => mapping(address => bool)) public hasVoted;

    constructor() {
        owner = msg.sender;
        members[msg.sender] = true;
    }

    function addMember(address member) public {
        require(msg.sender == owner, "only owner adds members");
        members[member] = true;
    }

    function propose(string memory description) public returns (uint256) {
        require(members[msg.sender], "only members propose");
        uint256 id = proposalCount;
        proposals[id].description = description;
        proposalCount += 1;
        return id;
    }

    function vote(uint256 id, bool support) public {
        require(members[msg.sender], "only members vote");
        require(id < proposalCount, "no such proposal");
        require(!hasVoted[id][msg.sender], "already voted");
        hasVoted[id][msg.sender] = true;
        if (support) {
            proposals[id].votesFor += 1;
        } else {
            proposals[id].votesAgainst += 1;
        }
    }

    function execute(uint256 id) public {
        require(members[msg.sender], "only members execute");
        require(id < proposalCount, "no such proposal");
        require(!proposals[id].executed, "already executed");
        require(proposals[id].votesFor > proposals[id].votesAgainst, "needs more votes for");
        proposals[id].executed = true;
    }
}

Key points

  • DAO: a group that makes decisions by member votes recorded on a contract.
  • Proposal: an idea a member puts forward for the group to decide.
  • Vote: one member choice for or against a single proposal.
  • One vote rule: each member votes at most once on each proposal.
  • Execution: carrying out a proposal once it has passed.
  • Majority: a proposal passes when votes for it outnumber votes against it.

In this example, a DAO with three members runs two proposals side by side. The first proposal wins two votes for and executes, while the second proposal draws only a vote against and stays unexecuted, and an executed proposal cannot execute a second time.

Note

the toy DAO counts members, not token weight, so every member counts the same.

Try it Yourself

Edit the code, then press Run.

Running is simulated · Remix IDE

Exercise

A proposal has 2 votes for and 1 vote against. What happens when a member executes it?