Cairo Academy: Understanding a Basic DAO Contract
This chapter explores a fundamental DAO (Decentralized Autonomous Organization) contract implemented in Cairo. This contract demonstrates core DAO functionalities, providing a practical example for learning how to build decentralized governance systems on StarkNet.
Purpose and Functionality
The provided Cairo code defines a basic DAO contract with the following capabilities:
- Proposal Management: Create, vote on, and execute governance proposals
- Membership Control: Add and remove members with voting power
- Voting System: Track votes for and against proposals
- Governance Rules: Enforce voting periods and execution conditions
Key Components
Data Structures
#![allow(unused)] fn main() { #[derive(Copy, Drop, Hash)] struct ProposalId { id: u64 } #[derive(Copy, Drop, Serde, starknet::Store)] struct Proposal { proposer: ContractAddress, description: felt252, for_votes: u256, against_votes: u256, start_block: u64, end_block: u64, executed: bool } #[derive(Copy, Drop, Serde, starknet::Store)] struct Member { address: ContractAddress, voting_power: u256 } }
Core Functionalities
-
Proposal Creation:
- Only members can create proposals
- Each proposal has a 100-block voting period
- Proposals store vote counts and execution status
-
Voting Mechanism:
- Members vote with their voting power
- Votes can be for or against proposals
- Voting is only allowed during the active period
-
Proposal Execution:
- Can only execute after voting period ends
- Requires more for-votes than against-votes
- Prevents double execution
-
Membership Management:
- Admin-controlled member additions/removals
- Members have associated voting power
Interface Definition
#![allow(unused)] fn main() { #[starknet::interface] pub trait IDAO<TContractState> { fn create_proposal(ref self: TContractState, description: felt252); fn vote(ref self: TContractState, proposal_id: ProposalId, support: bool); fn execute_proposal(ref self: TContractState, proposal_id: ProposalId); fn add_member(ref self: TContractState, member: ContractAddress, voting_power: u256); fn remove_member(ref self: TContractState, member: ContractAddress); fn get_proposal(self: @TContractState, proposal_id: ProposalId) -> Proposal; fn get_member(self: @TContractState, member: ContractAddress) -> Member; } }
Event System
#![allow(unused)] fn main() { #[event] enum Event { ProposalCreated: ProposalCreatedEvent, Voted: VotedEvent, ProposalExecuted: ProposalExecutedEvent, MemberAdded: MemberAddedEvent, MemberRemoved: MemberRemovedEvent } }
Security Features
- Member verification for proposal creation
- Voting period enforcement
- Execution condition checks
- Admin-only member management
Usage Example
- Admin adds members with
add_member
- Members create proposals with
create_proposal
- Other members vote with vote
- After voting period, anyone can execute_proposal if it passed
Full Implementation
#![allow(unused)] fn main() { use core::num::traits::Zero; use starknet::ContractAddress; #[derive(Copy, Drop, Hash)] struct ProposalId { id: u64 } #[derive(Copy, Drop, Serde, starknet::Store)] struct Proposal { proposer: ContractAddress, description: felt252, for_votes: u256, against_votes: u256, start_block: u64, end_block: u64, executed: bool } #[derive(Copy, Drop, Serde, starknet::Store)] struct Member { address: ContractAddress, voting_power: u256 } #[starknet::interface] pub trait IDAO<TContractState> { fn create_proposal(ref self: TContractState, description: felt252); fn vote(ref self: TContractState, proposal_id: ProposalId, support: bool); fn execute_proposal(ref self: TContractState, proposal_id: ProposalId); fn add_member(ref self: TContractState, member: ContractAddress, voting_power: u256); fn remove_member(ref self: TContractState, member: ContractAddress); fn get_proposal(self: @TContractState, proposal_id: ProposalId) -> Proposal; fn get_member(self: @TContractState, member: ContractAddress) -> Member; } #[starknet::contract] mod dao { use core::num::traits::Zero; use starknet::{ContractAddress, get_caller_address, get_block_number}; use super::{IDAO, ProposalId, Proposal, Member}; #[storage] struct Storage { proposals: LegacyMap<ProposalId, Proposal>, members: LegacyMap<ContractAddress, Member>, next_proposal_id: u64 } #[event] enum Event { ProposalCreated: ProposalCreatedEvent, Voted: VotedEvent, ProposalExecuted: ProposalExecutedEvent, MemberAdded: MemberAddedEvent, MemberRemoved: MemberRemovedEvent } #[derive(Drop, Serde)] struct ProposalCreatedEvent { proposal_id: ProposalId, proposer: ContractAddress, description: felt252, start_block: u64, end_block: u64 } #[derive(Drop, Serde)] struct VotedEvent { proposal_id: ProposalId, voter: ContractAddress, support: bool } #[derive(Drop, Serde)] struct ProposalExecutedEvent { proposal_id: ProposalId } #[derive(Drop, Serde)] struct MemberAddedEvent { member: ContractAddress, voting_power: u256 } #[derive(Drop, Serde)] struct MemberRemovedEvent { member: ContractAddress } #[abi(embed_v0)] impl IDAOImpl of IDAO<ContractState> { fn create_proposal(ref self: ContractState, description: felt252) { let caller: ContractAddress = get_caller_address(); let member: Member = self.members.read(caller); assert!(member.voting_power > Zero::zero(), "Caller is not a member"); let proposal_id = ProposalId { id: self.next_proposal_id.read() }; let start_block = get_block_number(); let end_block = start_block + 100; self.proposals.write(proposal_id, Proposal { proposer: caller, description, for_votes: Zero::zero(), against_votes: Zero::zero(), start_block, end_block, executed: false }); self.next_proposal_id.write(self.next_proposal_id.read() + 1); self.emit(ProposalCreatedEvent { proposal_id, proposer: caller, description, start_block, end_block }); } fn vote(ref self: ContractState, proposal_id: ProposalId, support: bool) { let caller: ContractAddress = get_caller_address(); let member: Member = self.members.read(caller); let mut proposal: Proposal = self.proposals.read(proposal_id); assert!(get_block_number() < proposal.end_block, "Voting period has ended"); assert!(!proposal.executed, "Proposal already executed"); if support { proposal.for_votes += member.voting_power; } else { proposal.against_votes += member.voting_power; } self.proposals.write(proposal_id, proposal); self.emit(VotedEvent { proposal_id, voter: caller, support }); } fn execute_proposal(ref self: ContractState, proposal_id: ProposalId) { let proposal: Proposal = self.proposals.read(proposal_id); assert!(get_block_number() > proposal.end_block, "Voting period has not ended"); assert!(!proposal.executed, "Proposal already executed"); assert!(proposal.for_votes > proposal.against_votes, "Proposal did not pass"); // Execute the proposal (e.g., transfer funds, update state, etc.) // Placeholder for proposal execution logic self.proposals.write(proposal_id, Proposal { executed: true, ..proposal }); self.emit(ProposalExecutedEvent { proposal_id }); } fn add_member(ref self: ContractState, member: ContractAddress, voting_power: u256) { let caller: ContractAddress = get_caller_address(); assert!(caller == self.admin.read(), "Only admin can add members"); self.members.write(member, Member { address: member, voting_power }); self.emit(MemberAddedEvent { member, voting_power }); } fn remove_member(ref self: ContractState, member: ContractAddress) { let caller: ContractAddress = get_caller_address(); assert!(caller == self.admin.read(), "Only admin can remove members"); self.members.write(member, Member { address: member, voting_power: Zero::zero() }); self.emit(MemberRemovedEvent { member }); } fn get_proposal(self: @ContractState, proposal_id: ProposalId) -> Proposal { self.proposals.read(proposal_id) } fn get_member(self: @ContractState, member: ContractAddress) -> Member { self.members.read(member) } } } }
This contract serves as a foundation for building more complex DAO systems on StarkNet, demonstrating key concepts like decentralized governance and member voting.