Abie

The easy fund

Intro

Abie is a crowdfunding app built on Ethereum.

Abie is a DAO that includes a voting system and a minimalist membership system resistant to Sybil attacks. Only the vote of the members can trigger a transaction to the beneficiary. This simple voting system called 'liquid democracy' allows participants that don't have any device or Internet access to express their opinion on each incoming proposal.

Anyone can deploy a DAO to take collective decisions and actions leveraging the best features of the Ethereum blockchain.

How to use Abie?

  • Deploy one contract (name, statement of intent, list of 1st members)
  • Create a fb/any group and display the official address
  • Submit and vote the 1st proposal mentioning the types of projects to be funded
  • Spread the address everywhere on the networks
  • Reach out similar orgs so they can join forces

Code

The following Solidity contract was made at Hacker House Paris with the help of Ethergency team on March 5, 2017.

Source: https://github.com/AbieFund/abie


/* Part of this contract is from the solidity documentation
Currently developed under the MIT License: https://github.com/AbieFund/abie/blob/master/LICENSE
*/

pragma solidity ^0.4.8;

/// @title Fund for donations.
contract Abie {

    uint public membershipFee = 0.1 ether;
    uint public deposit = 0.1 ether;
    uint public trigger = 0.1 ether;
    uint public nbMembers;
    uint public nbProposalsFund;
    uint public registrationTime = 1 years;
    uint[2] public voteLength = [1 minutes, 1 minutes];
    uint MAX_DELEGATION_DEPTH=10;
    address NOT_COUNTED=0;
    address COUNTED=1;

    event Donated(address donor, uint amount);

    enum ProposalType {AddMember,FundProject} // Different types of proposals.
    enum VoteType {Abstain,Yes,No} // Different value of a vote.

    struct Proposal
    {
        bytes32 name; // short name (up to 32 bytes).
        uint voteYes; // number of YES votes.
        uint voteNo;  // number of abstention. Number of No can be deduced.
        address recipient; // address the funds will be sent.
        uint value; // quantity of wei to be sent.
        bytes32 data; // data of the transaction.
        ProposalType proposalType; // type of the proposal.
        uint endDate; // when the vote will be closed.
        address lastMemberCounted; // last one who was counted or NOT_COUNTED (if the count has not started) or COUNTED (if all the votes has been counted);
        bool executed; // true if the proposal have been executed.
        mapping (address => VoteType) vote; // vote of the party.
    }

    // Is also a node list.
    struct Member
    {
        uint registration;  // date of registration, if 0 the member does not exist.
        address[2] delegate; // delegate[proposalType] gives the delegate for the type.
        address prev;
        address succ; // this should not be deleted even when the member is.
        uint proposalStoppedOnHim; // number of proposals stopped on him.
    }

    mapping (address => Member) public members;

    // Double chained list.
    struct DoubleChainedList
    {
        address first;
        address last;
    }

    // Chain containing all members to iterate on.
    DoubleChainedList memberList;

    Proposal[] public proposals;

    /// Require at least price to be paid.
    modifier costs(uint price) {
        require(msg.value>=price);
        _;
    }

    /// Require the caller to be a member.
    modifier isMember() {
        require(isValidMember(msg.sender));
        _;
    }


    /// @param initialMembers First members of the organization.
    function Abie(address[] initialMembers) public{
        for (uint i;i<`initialMembers.length;++i){
            Member storage member=members[initialMembers[i]];
            member.registration=now;
            if (i==0) { // initialize the list with the first member
                memberList.first=initialMembers[0];
                memberList.last=initialMembers[0];
            } else { // add members
                addMember(initialMembers[i]);
            }
        }
        nbMembers=initialMembers.length;
        // Set default member.
        if (nbMembers==0){
        addMember(msg.sender);
        }
 }

    // Add the member m to the member list.
    // Assume that there is at least 1 member registrated.
    function addMember(address m) private {
        members[memberList.last].succ=m;
        members[m].prev=memberList.last;
        memberList.last=m;
    }

    /** Choose a delegate.
      * @param proposalType 0 for AddMember, 1 for FundProject.
      * @param target account to delegate to.
      */
    function setDelegate(uint8 proposalType, address target) public
    {
        members[msg.sender].delegate[proposalType] = target;
    }

    /// Receive funds.
    function () payable public{
        Donated(msg.sender, msg.value);
    }

    /// Ask for membership.
    function askMembership () payable public costs(membershipFee) {
         Donated(msg.sender,msg.value); // Register the donation.

        // Create a proposal to add the member.
        proposals.push(Proposal({
          name: 0x0,
          voteYes: 0,
          voteNo: 0,
          recipient: msg.sender,
          value: 0x0,
          data: 0x0,
          proposalType: ProposalType.AddMember,
              endDate: now + 1 minutes,
          //voteLength[uint256(ProposalType.AddMember)],
          lastMemberCounted: 0,
          executed: false
        }));
    }

    /// Add Proposal.
    function addProposal (bytes32 _name, uint _value, bytes32 _data) payable public costs(deposit) {
        Donated(msg.sender,msg.value); // Register the donation.

        // Create a proposal to add the member.
        proposals.push(Proposal({
          name: _name,
          voteYes: 0,
          voteNo: 0,
          recipient: msg.sender,
          value: _value,
          data: _data,
          proposalType: ProposalType.FundProject,
          endDate: now + 1 minutes,
          lastMemberCounted: 0,
          executed: false
        }));

        ++nbProposalsFund;
    }

    /** Vote for a proposal.
     *  @param proposalID ID of the proposal to count votes from.
     *  @param voteType Yes or No.
     */
    function vote (uint proposalID, VoteType voteType) public isMember {
        Proposal storage proposal = proposals[proposalID];
        require(proposal.vote[msg.sender] == VoteType.Abstain); // Has already voted.
        require(proposal.endDate >= now); // Vote is over.
        proposals[proposalID].vote[msg.sender] = voteType;
    }

    /** Count all the votes. You can call this function if gas limit is not an issue.
     *  @param proposalID ID of the proposal to count votes from.
     */
    function countAllVotes (uint proposalID) public{
        countVotes (proposalID,uint(-1));
    }

    /** Count up to max of the votes.
     *  You may have to call this function multiple times if counting once reach the gas limit.
     *  This function is necessary to count in multiple times if counting reach gas limit.
     *  We just count the number of Yes and Abstention, so we will deduce the number of No.
     *  @param proposalID ID of the proposal to count votes from.
     *  @param max maximum to count.
     */
    function countVotes (uint proposalID, uint max) public {
        Proposal storage proposal = proposals[proposalID];
        address current;
        require (proposal.endDate <= now); // You can't count while the vote is not over.
        require (proposal.lastMemberCounted != COUNTED); // The count is already over

        if (proposal.lastMemberCounted == NOT_COUNTED)
            current = memberList.first;
        else
            current = proposal.lastMemberCounted;

        while (max-- != 0) {
            Member storage member = members[current];
            address delegate=current;
            if(isValidMember(current)) {
                uint depth=0;
                // Search the final vote.
                while (true){
                    VoteType voteNow = proposal.vote[delegate];
                    if (voteNow==VoteType.Abstain) { // Look at the delegate
                        depth+=1;
                        delegate=members[delegate].delegate[uint(proposal.proposalType)]; // Find the delegate.
                        if (delegate==current // The delegation chain forms a circle.
                            || delegate==0  // Has not set a delegate.
                            || depth>MAX_DELEGATION_DEPTH) { // Too much depth, we must limit it in order to avoid some circle of delegation made to consume too much gaz.
                           break;
                        }
                    }
                    if (voteNow==VoteType.Yes) {
                        proposal.voteYes+=1;
                        break;
                    } else if (voteNow==VoteType.No) {
                        proposal.voteNo+=1;
                        break;
                    }
                }
            } else {
                // TODO: Delete the members if they are expired.
            }

            current=member.succ; // In next iteration start from the next node.
            if (current==0) { // We reached the last member.
                proposal.lastMemberCounted=COUNTED;
                break;
            }
        }
    }

    function executeAddMemberProposal(uint proposalID) public {
        Proposal storage proposal = proposals[proposalID];
        require (proposal.proposalType == ProposalType.AddMember); // Not a proposal to add a member.
        require (isExecutable(proposalID)); // Proposal was not approved.
        proposal.executed=true; // The proposal will be executed.
        addMember(proposal.recipient);
    }

    // The following function has NOT been reviewed so far.
    function claim(uint proposalID) public payable costs(trigger) {
        Proposal storage proposal = proposals[proposalID];
        address beneficiary = proposal.recipient;
        uint value = proposal.value;
        uint check = trigger+deposit+value;
        require (isExecutable(proposalID)); // Si la proposal n'est pas exécutable, dégage.
        require(proposal.executed == false); // Si c'est déjà exécuté, dégage.
        require (beneficiary == msg.sender); // si pas bénéficiaire, dégage.
        proposal.executed = true; // The proposal was executed.
        beneficiary.transfer(check); // The beneficiary gets the requested amount.
    }

    /// CONSTANTS ///

    /** Return the delegate.
     *  @param member member to get the delegate from.
     *  @param proposalType 0 for AddMember, 1 for FundProject.
     */
    function getDelegate(address member, uint8 proposalType) public constant returns (address){
        return members[member].delegate[proposalType];
    }

    /** Return true if the proposal is validated, false otherwise.
     *  @param proposalID ID of the proposal to count votes from.
     */
    function isExecutable(uint proposalID) public constant returns (bool) {
        Proposal storage proposal = proposals[proposalID];

        if (proposal.lastMemberCounted != COUNTED) // Not counted yet.
            return false;
        if (proposal.executed) // The proposal has already been executed.
            return false;
        if (proposal.value > address(this).balance) // Not enough to execute it.
            return false;

        return (proposal.voteYes>proposal.voteNo);
    }

    function isValidMember(address m) public constant returns(bool) {
        if (members[m].registration==0) // Not a member.
            return false;
        if (members[m].registration+registrationTime<`now) // Has expired.
            return false;
        return true;
    }

    function contractBalance() public constant returns(uint256) {
        return address(this).balance;
    }

 function timeLeft(uint proposalID) public constant returns(uint256) {
        Proposal storage proposal = proposals[proposalID];
        return proposal.endDate - now;
    }
}

Next steps

  • Run a basic campaign on main net. Add 10 ETH in the contract and call for attacks
  • Measure the participation and relevancy rates.
  • Improve the UI

Feel free to join our Slack or Abie Riot/IRC chan. We're on Twitter and Facebook too.



Up: May 4, 2018