https://github.com/edjcase/motoko_proposal_engine
Motoko library for creating, voting on and executing proposals
https://github.com/edjcase/motoko_proposal_engine
Last synced: 5 months ago
JSON representation
Motoko library for creating, voting on and executing proposals
- Host: GitHub
- URL: https://github.com/edjcase/motoko_proposal_engine
- Owner: edjCase
- License: mit
- Created: 2024-07-19T21:14:49.000Z (almost 2 years ago)
- Default Branch: main
- Last Pushed: 2025-08-01T23:29:18.000Z (11 months ago)
- Last Synced: 2025-08-08T05:32:27.510Z (11 months ago)
- Language: Motoko
- Size: 148 KB
- Stars: 2
- Watchers: 1
- Forks: 1
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Overview
A library for creating, voting on and executing proposals
# Package
### MOPS
# Motoko Proposal Engine
A comprehensive library for creating, voting on, and executing proposals in Motoko. This library supports both simple boolean voting and advanced multi-choice voting with configurable thresholds and voting modes.
## Features
- **Multiple Voting Modes**: Snapshot-based and dynamic voting
- **Flexible Choices**: Boolean voting or custom choice types
- **Configurable Thresholds**: Percentage-based voting with optional quorum
- **Dynamic Member Management**: Add members to proposals during voting
- **Automatic Execution**: Proposals execute automatically when thresholds are met
- **Time-bound Voting**: Optional proposal durations with automatic ending
- **Stable Upgrades**: Full support for canister upgrades
## Package
### MOPS
```bash
mops install dao-proposal-engine
```
To setup MOPS package manage, follow the instructions from the [MOPS Site](https://j4mwm-bqaaa-aaaam-qajbq-cai.ic0.app/)
## Quick Start
### Simple Boolean Voting
```motoko
import ProposalEngine "mo:dao-proposal-engine/ProposalEngine";
// Initialize with stable data
let stableData = {
proposals = BTree.init>(null);
proposalDuration = ?#days(7); // 7 day voting period
votingThreshold = #percent({ percent = 50; quorum = ?25 });
allowVoteChange = false;
};
// Create proposal engine for boolean voting
let engine = ProposalEngine.ProposalEngine(
stableData,
onProposalAdopt, // Called when proposal passes
onProposalReject, // Called when proposal fails
onProposalValidate // Validates proposal content
);
// Create a proposal
let members = [
{ id = principalA; votingPower = 100 },
{ id = principalB; votingPower = 50 }
];
let proposalId = await* engine.createProposal(
proposerId,
proposalContent,
members,
#snapshot // Snapshot voting mode
);
// Vote on proposal
let _ = await* engine.vote(proposalId, voterId, true); // Vote yes
```
### Advanced Multi-Choice Voting
```motoko
import ExtendedProposalEngine "mo:dao-proposal-engine/ExtendedProposalEngine";
// Create proposal engine for custom choice voting
let engine = ExtendedProposalEngine.ProposalEngine(
stableData,
onProposalExecute, // Called with winning choice
onProposalValidate, // Validates proposal content
MyChoice.compare, // Choice compare function
);
// Create proposal with dynamic voting
let proposalId = await* engine.createProposal(
proposerId,
proposalContent,
members,
#dynamic({ totalVotingPower = ?1000 }) // Dynamic voting mode
);
// Add member during voting (only for dynamic mode)
let newMember = { id = newPrincipal; votingPower = 75 };
let _ = engine.addMember(proposalId, newMember);
// Vote with custom choice
let _ = await* engine.vote(proposalId, voterId, myChoice);
```
## Architecture Overview
### Proposal vs Engine
The library provides two levels of abstraction for working with proposals:
#### **Proposal Modules** (`Proposal.mo` and `ExtendedProposal.mo`)
- **Pure data structures**: Hold proposal data and voting information
- **Stateless functions**: Provide utilities for voting, calculating status, and managing proposal data
- **Manual management**: You handle storage, timers, and state transitions yourself
- **Direct control**: Full control over when and how proposals are processed
```motoko
// Direct proposal management
import Proposal "mo:dao-proposal-engine/Proposal";
let proposal = Proposal.create(...);
let voteResult = Proposal.vote(proposal, voterId, true, allowVoteChange);
let status = Proposal.calculateVoteStatus(proposal, threshold, forceEnd);
// You handle storage and execution yourself
```
#### **Engine Classes** (`ProposalEngine.mo` and `ExtendedProposalEngine.mo`)
- **Complete management system**: Handles proposal storage, lifecycle, and execution
- **Automatic features**:
- Timer-based proposal ending
- Automatic status transitions
- Auto-execution when thresholds are met
- Stable data management for upgrades
- **Event-driven**: Callbacks for proposal adoption, rejection, and validation
- **Production-ready**: Handles all the complex state management for you
```motoko
// Managed proposal system
import ProposalEngine "mo:dao-proposal-engine/ProposalEngine";
let engine = ProposalEngine.ProposalEngine(...);
let proposalId = await* engine.createProposal(...); // Stored automatically
let _ = await* engine.vote(proposalId, voterId, true); // Auto-executes if threshold met
// Engine handles timers, storage, and execution automatically
```
### Standard vs Extended Proposals
#### **Standard Proposals** (`Proposal.mo` and `ProposalEngine.mo`)
- **Boolean voting**: Simple adopt (true) or reject (false) decisions
- **Two outcomes**: Proposals either pass or fail
- **Simplified API**: Easier to use for basic governance needs
- **Type safety**: Enforced boolean voting prevents choice errors
```motoko
// Boolean voting - simple and clear
let _ = await* engine.vote(proposalId, voterId, true); // Vote to adopt
let _ = await* engine.vote(proposalId, voterId, false); // Vote to reject
```
#### **Extended Proposals** (`ExtendedProposal.mo` and `ExtendedProposalEngine.mo`)
- **Custom choice types**: Any type can be used for voting choices
- **Multi-choice voting**: Support for complex decision-making scenarios
- **Flexible outcomes**: Winners determined by plurality or custom logic
- **Advanced scenarios**: Budget allocation, candidate selection, configuration options
```motoko
// Multi-choice voting with custom types
type BudgetChoice = {
#allocateToMarketing: Nat;
#allocateToEngineering: Nat;
#allocateToOperations: Nat;
#rejectBudget;
};
let _ = await* extendedEngine.vote(proposalId, voterId, #allocateToEngineering(500_000));
```
## API Reference
### Core Types
#### StableData
```motoko
type StableData = {
proposals : [Proposal];
proposalDuration : ?Duration;
votingThreshold : VotingThreshold;
allowVoteChange : Bool;
};
```
#### PagedResult
```motoko
type PagedResult = {
data : [T];
offset : Nat;
count : Nat;
totalCount : Nat;
};
```
#### VotingMode
```motoko
type VotingMode = {
#snapshot; // Fixed member list at creation
#dynamic : { totalVotingPower : ?Nat }; // Members can be added during voting
};
```
#### VotingThreshold
```motoko
type VotingThreshold = {
#percent : { percent : Nat; quorum : ?Nat }; // Percentage (0-100) with optional quorum
};
```
#### Duration
```motoko
type Duration = {
#days : Nat;
#nanoseconds : Nat;
};
```
#### Member
```motoko
type Member = {
id : Principal;
votingPower : Nat;
};
```
#### Proposal
```motoko
type Proposal = {
id : Nat;
proposerId : Principal;
timeStart : Int;
timeEnd : ?Int;
votingMode : VotingMode;
content : TProposalContent;
votes : BTree>;
status : ProposalStatus;
};
```
#### ProposalStatus
```motoko
type ProposalStatus = {
#open;
#executing : { executingTime : Time; choice : ?TChoice };
#executed : { executingTime : Time; executedTime : Time; choice : ?TChoice };
#failedToExecute : { executingTime : Time; failedTime : Time; choice : ?TChoice; error : Text };
};
```
#### Vote
```motoko
type Vote = {
choice : ?TChoice;
votingPower : Nat;
};
```
#### VotingSummary
```motoko
type VotingSummary = {
votingPowerByChoice : [ChoiceVotingPower];
totalVotingPower : Nat;
undecidedVotingPower : Nat;
};
```
#### ChoiceVotingPower
```motoko
type ChoiceVotingPower = {
choice : TChoice;
votingPower : Nat;
};
```
### Error Types
#### VoteError
```motoko
type VoteError = {
#notEligible; // Voter is not a member of the proposal
#alreadyVoted; // Voter has already voted (when vote changes are disabled)
#votingClosed; // Voting period has ended or proposal is not open
#proposalNotFound; // Proposal ID does not exist (ExtendedProposalEngine only)
};
```
#### CreateProposalError
```motoko
type CreateProposalError = {
#notEligible; // Proposer is not eligible to create proposals
#invalid : [Text]; // Proposal content failed validation
};
```
#### AddMemberResult
```motoko
type AddMemberResult = {
#ok; // Member added successfully
#alreadyExists; // Member already exists in the proposal
#proposalNotFound; // Proposal ID does not exist
#votingNotDynamic; // Proposal is not in dynamic voting mode
#votingClosed; // Voting period has ended
};
```
### ProposalEngine (Boolean Voting)
#### Constructor
```motoko
ProposalEngine(
data: StableData,
onProposalAdopt: Proposal -> async* Result.Result<(), Text>,
onProposalReject: Proposal -> async* (),
onProposalValidate: TProposalContent -> async* Result.Result<(), [Text])
)
```
#### Methods
**`getProposal(id: Nat) : ?Proposal`**
Returns a proposal by its ID.
**`getProposals(count: Nat, offset: Nat) : PagedResult>`**
Retrieves a paged list of proposals, sorted by creation time (newest first).
**`getVote(proposalId: Nat, voterId: Principal) : ?Vote`**
Retrieves a specific voter's vote on a proposal.
**`buildVotingSummary(proposalId: Nat) : VotingSummary`**
Builds a voting summary showing vote tallies and statistics.
**`vote(proposalId: Nat, voterId: Principal, vote: Bool) : async* Result.Result<(), VoteError>`**
Casts a vote on a proposal. Returns error if voter is not eligible or voting is closed.
**`createProposal(proposerId: Principal, content: TProposalContent, members: [Member], votingMode: VotingMode) : async* Result.Result`**
Creates a new proposal. Returns the proposal ID on success.
**`addMember(proposalId: Nat, member: Member) : Result.Result<(), AddMemberResult>`**
Adds a member to a dynamic proposal during voting.
**`endProposal(proposalId: Nat) : async* Result.Result<(), { #alreadyEnded }>`**
Manually ends a proposal before its natural end time.
**`toStableData() : StableData`**
Converts the current state to stable data for upgrades.
### ExtendedProposalEngine (Multi-Choice Voting)
#### Constructor
```motoko
ProposalEngine(
data: StableData,
onProposalExecute: (?TChoice, Proposal) -> async* Result.Result<(), Text>,
onProposalValidate: TProposalContent -> async* Result.Result<(), [Text]),
compareChoice: (TChoice, TChoice) -> Order.Order,
)
```
#### Methods
**`getProposal(id: Nat) : ?Proposal`**
Returns a proposal by its ID.
**`getProposals(count: Nat, offset: Nat) : PagedResult>`**
Retrieves a paged list of proposals, sorted by creation time (newest first).
**`getVote(proposalId: Nat, voterId: Principal) : ?Vote`**
Retrieves a specific voter's vote on a proposal.
**`buildVotingSummary(proposalId: Nat) : VotingSummary`**
Builds a voting summary showing vote tallies and statistics.
**`vote(proposalId: Nat, voterId: Principal, vote: TChoice) : async* Result.Result<(), VoteError>`**
Casts a vote on a proposal with a custom choice type.
**`createProposal(proposerId: Principal, content: TProposalContent, members: [Member], votingMode: VotingMode) : async* Result.Result`**
Creates a new proposal. Returns the proposal ID on success.
**`addMember(proposalId: Nat, member: Member) : Result.Result<(), AddMemberResult>`**
Adds a member to a dynamic proposal during voting.
**`endProposal(proposalId: Nat) : async* Result.Result<(), { #alreadyEnded }>`**
Manually ends a proposal before its natural end time.
**`toStableData() : StableData`**
Converts the current state to stable data for upgrades.
## Voting Modes
### Snapshot Mode (`#snapshot`)
- Member list is fixed at proposal creation
- No members can be added during voting
- Suitable for formal governance where membership is predetermined
### Dynamic Mode (`#dynamic`)
- Members can be added during the voting period
- Optionally specify total voting power for threshold calculations
- Suitable for evolving communities or stake-based voting
## Voting Thresholds
### Percentage Threshold
```motoko
#percent({ percent = 50; quorum = ?25 })
```
- `percent`: Required percentage of votes to pass (0-100)
- `quorum`: Optional minimum participation percentage
**Threshold Calculation:**
- Before proposal end: Threshold applies to total possible voting power
- After proposal end: Threshold applies only to votes cast
- Dynamic proposals: Stay undetermined even when threshold is met (manual execution required)
## Examples
### Governance Proposal
```motoko
type GovernanceProposal = {
title: Text;
description: Text;
action: {
#updateConfig: { key: Text; value: Text };
#addMember: Principal;
#removeMember: Principal;
};
};
let proposal = await* engine.createProposal(
caller,
{
title = "Update Configuration";
description = "Change max proposal duration to 14 days";
action = #updateConfig({ key = "maxDuration"; value = "14" });
},
members,
#snapshot
);
```
### Multi-Choice Budget Proposal
```motoko
type BudgetChoice = {
#allocateToMarketing: Nat;
#allocateToEngineering: Nat;
#allocateToOperations: Nat;
#rejectBudget;
};
let proposalId = await* extendedEngine.createProposal(
caller,
budgetProposalContent,
stakeholders,
#dynamic({ totalVotingPower = ?totalStake })
);
// Stakeholders vote on budget allocation
let _ = await* extendedEngine.vote(proposalId, stakeholderA, #allocateToEngineering(500_000));
let _ = await* extendedEngine.vote(proposalId, stakeholderB, #allocateToMarketing(300_000));
```
## Testing
```bash
mops test
```
## License
This project is licensed under the MIT License.