A library for creating, voting on and executing proposals
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.
- 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
mops install dao-proposal-engineTo setup MOPS package manage, follow the instructions from the MOPS Site
import ProposalEngine "mo:dao-proposal-engine/ProposalEngine";
// Initialize with stable data
let stableData = {
proposals = BTree.init<Nat, ProposalEngine.ProposalData<MyProposalContent>>(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<system, MyProposalContent>(
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 yesimport ExtendedProposalEngine "mo:dao-proposal-engine/ExtendedProposalEngine";
// Create proposal engine for custom choice voting
let engine = ExtendedProposalEngine.ProposalEngine<system, MyProposalContent, MyChoice>(
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);The library provides two levels of abstraction for working with proposals:
- 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
// 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- 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
// Managed proposal system
import ProposalEngine "mo:dao-proposal-engine/ProposalEngine";
let engine = ProposalEngine.ProposalEngine<system, MyContent>(...);
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- 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
// 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- 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
// Multi-choice voting with custom types
type BudgetChoice = {
#allocateToMarketing: Nat;
#allocateToEngineering: Nat;
#allocateToOperations: Nat;
#rejectBudget;
};
let _ = await* extendedEngine.vote(proposalId, voterId, #allocateToEngineering(500_000));type StableData<TProposalContent, TChoice> = {
proposals : [Proposal<TProposalContent, TChoice>];
proposalDuration : ?Duration;
votingThreshold : VotingThreshold;
allowVoteChange : Bool;
};type PagedResult<T> = {
data : [T];
offset : Nat;
count : Nat;
totalCount : Nat;
};type VotingMode = {
#snapshot; // Fixed member list at creation
#dynamic : { totalVotingPower : ?Nat }; // Members can be added during voting
};type VotingThreshold = {
#percent : { percent : Nat; quorum : ?Nat }; // Percentage (0-100) with optional quorum
};type Duration = {
#days : Nat;
#nanoseconds : Nat;
};type Member = {
id : Principal;
votingPower : Nat;
};type Proposal<TProposalContent, TChoice> = {
id : Nat;
proposerId : Principal;
timeStart : Int;
timeEnd : ?Int;
votingMode : VotingMode;
content : TProposalContent;
votes : BTree<Principal, Vote<TChoice>>;
status : ProposalStatus<TChoice>;
};type ProposalStatus<TChoice> = {
#open;
#executing : { executingTime : Time; choice : ?TChoice };
#executed : { executingTime : Time; executedTime : Time; choice : ?TChoice };
#failedToExecute : { executingTime : Time; failedTime : Time; choice : ?TChoice; error : Text };
};type Vote<TChoice> = {
choice : ?TChoice;
votingPower : Nat;
};type VotingSummary<TChoice> = {
votingPowerByChoice : [ChoiceVotingPower<TChoice>];
totalVotingPower : Nat;
undecidedVotingPower : Nat;
};type ChoiceVotingPower<TChoice> = {
choice : TChoice;
votingPower : Nat;
};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)
};type CreateProposalError = {
#notEligible; // Proposer is not eligible to create proposals
#invalid : [Text]; // Proposal content failed validation
};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<system, TProposalContent>(
data: StableData<TProposalContent>,
onProposalAdopt: Proposal<TProposalContent> -> async* Result.Result<(), Text>,
onProposalReject: Proposal<TProposalContent> -> async* (),
onProposalValidate: TProposalContent -> async* Result.Result<(), [Text])
)getProposal(id: Nat) : ?Proposal<TProposalContent>
Returns a proposal by its ID.
getProposals(count: Nat, offset: Nat) : PagedResult<Proposal<TProposalContent>>
Retrieves a paged list of proposals, sorted by creation time (newest first).
getVote(proposalId: Nat, voterId: Principal) : ?Vote<Bool>
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<system>(proposerId: Principal, content: TProposalContent, members: [Member], votingMode: VotingMode) : async* Result.Result<Nat, CreateProposalError>
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<TProposalContent>
Converts the current state to stable data for upgrades.
ProposalEngine<system, TProposalContent, TChoice>(
data: StableData<TProposalContent, TChoice>,
onProposalExecute: (?TChoice, Proposal<TProposalContent, TChoice>) -> async* Result.Result<(), Text>,
onProposalValidate: TProposalContent -> async* Result.Result<(), [Text]),
compareChoice: (TChoice, TChoice) -> Order.Order,
)getProposal(id: Nat) : ?Proposal<TProposalContent, TChoice>
Returns a proposal by its ID.
getProposals(count: Nat, offset: Nat) : PagedResult<Proposal<TProposalContent, TChoice>>
Retrieves a paged list of proposals, sorted by creation time (newest first).
getVote(proposalId: Nat, voterId: Principal) : ?Vote<TChoice>
Retrieves a specific voter's vote on a proposal.
buildVotingSummary(proposalId: Nat) : VotingSummary<TChoice>
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<system>(proposerId: Principal, content: TProposalContent, members: [Member], votingMode: VotingMode) : async* Result.Result<Nat, CreateProposalError>
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<TProposalContent, TChoice>
Converts the current state to stable data for upgrades.
- Member list is fixed at proposal creation
- No members can be added during voting
- Suitable for formal governance where membership is predetermined
- Members can be added during the voting period
- Optionally specify total voting power for threshold calculations
- Suitable for evolving communities or stake-based voting
#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)
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
);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));mops testThis project is licensed under the MIT License.