Skip to content

Zaratti/SuiSmartContractVoting

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 

Repository files navigation

Basic Voting System on Sui Move

This READMe.md outlines a robust design and clear approach for a basic voting system, providing a comprehensive guide from project setup to implementation and deployment.

1. Introduction

We aim to develop a secure and transparent voting system using Sui Move smart contract. The system will allow for the creation of elections, registration of eligible voters using their public address, casting of votes, and transparent retrieval of results.

2. The Electoral Process

This revolves around the concept of an "Election" where participants ("Voters") cast their preferences for "Candidates." The system must ensure that only eligible voters can participate, votes are counted accurately, and results are tamper-proof. This model prioritizes the Election as the central Aggregate Root, managing its internal consistency.

3. Core Domain Concepts

3.1. Aggregates & Aggregate Roots

  • Election (Aggregate Root): Represents a single, distinct voting event. It is the central entity that encapsulates all state and behavior related to a specific election. All operations related to a particular election (e.g., registering voters, casting votes, getting results) must go through the Election object to maintain consistency and integrity.

    • Identity: Unique UID (Sui Object ID).

    • State: election_id, creator (address of the deployer/admin), name, description, start_time, end_time, is_active, is_finalized, candidates (Table of Candidate IDs to Candidate objects), voters (Table of Voter addresses to VoterStatus), vote_counts (Table of Candidate IDs to u64 vote counts).

3.2. Entities

  • Candidate: Represents an individual or option that can receive votes in an Election. It is an entity within the Election aggregate.

    • Identity: Unique UID.

    • State: id (UID), election_id (reference to the parent Election's ID), name (String).

  • (Note: For simplicity, a Voter is represented by their address and a VoterStatus value object within the Election's voters table, rather than a separate Sui object, to optimize for common access patterns and reduce object overhead.)

3.3. Value Objects

  • VoterStatus: A simple struct indicating whether a voter has cast their vote.

    • State: has_voted: bool.
  • VoteResult: A struct used to present election results.

    • State: candidate_name: String, votes: u64.
  • String: Move's String type for names and descriptions.

  • u64: Used for timestamps and vote counts.

4. Bounded Contexts

For this basic voting system, we will operate within a single Bounded Context: Voting.

  • Voting Bounded Context: Encompasses all the core logic and data structures for managing elections, voters, candidates, and votes.

    • Ubiquitous Language: Election, Candidate, Voter, Vote, Cast Vote, Register Voter, Finalize Election, Election Results.

5. Ubiquitous Language

  • Election: A specific voting event.

  • Candidate: An entity that can be voted for.

  • Voter: An individual eligible to cast a vote.

  • Vote: The act of casting a preference.

  • Cast Vote: The action of a voter submitting their choice.

  • Add Candidate: The action of adding a candidate to an election.

  • Register Voter: The action of adding an eligible participant to an election.

  • Finalize Election: The action of closing an election and making results immutable.

  • Election Results: The final tally of votes for each candidate.

6. Project Structure and Implementation Steps

This practical steps to implement the basic voting system using Sui Move.

6.1. Project/Directory Structure

A standard Sui Move project adheres to a specific directory structure.

scorebox-voting/ ├── Move.toml # Package manifest (defines dependencies, addresses) └── sources/ └── voting.move # Our main Move module for the voting system └── tests/ └── voting_tests.move # Unit tests for our voting module

6.2. Step 1: Initialize the Sui Project

First, ensure you have the Sui CLI installed. Then, create a new Sui Move project:

sui move new scorebox-voting

This command will create the scorebox-voting directory with the basic Move.toml and sources folder.

6.3. Step 2: Configure Move.toml

Open scorebox-voting/Move.toml. This file defines your package and its dependencies. For a new project, you'll primarily configure the [package] and [addresses] sections.

scorebox-voting/Move.toml

[package] name = "scorebox-voting" version = "0.0.1"

Replace '0x0' with your actual Sui address after publishing for devnet/testnet

For local testing, '0x0' is fine or use a placeholder like 'voting_system'

Then, when publishing, Sui CLI will replace '0x0' with the actual address.

Or, define a named address like 'voting_system' and replace '0x0' with 'voting_system' in your code.

For simplicity, we'll use '0x0' in the code directly for initial local testing.

published-at = "0x0"

[dependencies] Sui = { git = "https://github.com/MystenLabs/sui.git", subdir = "crates/sui-framework/packages/sui-framework", rev = "testnet" }

[addresses]

Define a named address for your module.

This will be replaced by the actual package ID upon deployment.

voting_system = "0x0" # Placeholder, will be replaced by Sui CLI on publish

6.4. Step 3: Define Core Structs in sources/voting.move

Now, let's define our core domain objects (Election, Candidate, VoterStatus) as Sui Move structs.

// scorebox-voting/sources/voting.move module scorebox_voting::voting { use sui::object::{Self, UID, ID}; use sui::tx_context::{Self, TxContext}; use sui::transfer; use sui::table::{Self, Table}; use sui::string::{Self, String};

// --- Aggregates & Entities ---

/// The Election Aggregate Root. This is a Shared Object.
/// It encapsulates all data and logic for a single voting event.
struct Election has key, store {
    id: UID,
    creator: address, // The address that created this election (admin)
    name: String,
    description: String,
    start_time: u64, // Unix timestamp for when voting begins
    end_time: u64,   // Unix timestamp for when voting ends
    is_active: bool, // True if voting is currently open
    is_finalized: bool, // True if election is closed and results are final

    // Candidates are stored in a Table, mapping their UID to the Candidate object.
    // This allows dynamic addition/removal and efficient lookup.
    candidates: Table<ID, Candidate>,

    // Voters are tracked by their address, and a boolean indicating if they've voted.
    // Using a Table<address, VoterStatus> is efficient for checking participation.
    voters: Table<address, VoterStatus>,

    // Vote counts for each candidate, mapping Candidate UID to the total votes.
    vote_counts: Table<ID, u64>,
}

/// Candidate Entity. Owned by the Election Aggregate.
struct Candidate has key, store {
    id: UID,
    election_id: ID, // ID of the parent Election object
    name: String,
}

// --- Value Objects ---

/// VoterStatus Value Object. Stored directly within the Election's 'voters' Table.
struct VoterStatus has store {
    has_voted: bool,
}

/// VoteResult Value Object. Used for returning structured results.
struct VoteResult has copy, drop, store {
    candidate_name: String,
    votes: u64,
}

// --- Error Codes ---
const E_NOT_CREATOR: u64 = 0;
const E_ELECTION_NOT_ACTIVE: u64 = 1;
const E_ELECTION_FINALIZED: u64 = 2;
const E_ALREADY_REGISTERED: u64 = 3;
const E_NOT_REGISTERED: u64 = 4;
const E_ALREADY_VOTED: u64 = 5;
const E_CANDIDATE_NOT_FOUND: u64 = 6;
const E_ELECTION_NOT_FINALIZED: u64 = 7;
const E_ELECTION_NOT_STARTED: u64 = 8;
const E_ELECTION_ENDED: u64 = 9;

// --- Public Functions (Entry Points) ---

/// Creates a new Election object.
/// This function creates a new shared object that represents an election.
public entry fun create_election(
    name: vector<u8>,
    description: vector<u8>,
    start_time: u64,
    end_time: u64,
    ctx: &mut TxContext,
) {
    // Basic time validation (start < end)
    assert!(start_time < end_time, E_ELECTION_ENDED); // More robust time checks can be added

    let election = Election {
        id: object::new(ctx),
        creator: tx_context::sender(ctx),
        name: string::utf8(name),
        description: string::utf8(description),
        start_time,
        end_time,
        is_active: false, // Starts inactive, needs to be activated
        is_finalized: false,
        candidates: table::new(ctx),
        voters: table::new(ctx),
        vote_counts: table::new(ctx),
    };

    // Transfer the newly created Election object as a shared object.
    // This makes it accessible for anyone to interact with via entry functions.
    transfer::share_object(election);
}

/// Adds a candidate to an existing election.
/// Only the election creator can add candidates, and only before the election starts.
public entry fun add_candidate(
    election: &mut Election,
    candidate_name: vector<u8>,
    ctx: &mut TxContext,
) {
    // Ensure only the creator can add candidates
    assert!(tx_context::sender(ctx) == election.creator, E_NOT_CREATOR);
    // Ensure election is not active or finalized
    assert!(!election.is_active, E_ELECTION_NOT_ACTIVE);
    assert!(!election.is_finalized, E_ELECTION_FINALIZED);

    let candidate = Candidate {
        id: object::new(ctx),
        election_id: object::id(election),
        name: string::utf8(candidate_name),
    };

    // Add the candidate to the election's candidates table
    table::add(&mut election.candidates, object::id(&candidate), candidate);
}

/// Activates an election, allowing votes to be cast.
/// Only the election creator can activate.
public entry fun activate_election(
    election: &mut Election,
    ctx: &TxContext,
) {
    assert!(tx_context::sender(ctx) == election.creator, E_NOT_CREATOR);
    assert!(!election.is_active, E_ELECTION_NOT_ACTIVE); // Must not be active already
    assert!(!election.is_finalized, E_ELECTION_FINALIZED); // Must not be finalized

    // Check if current time is within start and end times (requires Sui's clock object for real time)
    // For simplicity, we'll assume time checks are handled off-chain or by a trusted oracle for now.
    // In a real scenario, you'd use `sui::clock::timestamp_ms(clock_object)`

    election.is_active = true;
}

/// Registers a voter for a specific election.
/// Only the election creator can register voters, and only while the election is active.
public entry fun register_voter(
    election: &mut Election,
    voter_address: address,
    ctx: &TxContext,
) {
    assert!(tx_context::sender(ctx) == election.creator, E_NOT_CREATOR);
    assert!(election.is_active, E_ELECTION_NOT_ACTIVE);
    assert!(!election.is_finalized, E_ELECTION_FINALIZED);
    // Ensure voter is not already registered
    assert!(!table::contains(&election.voters, voter_address), E_ALREADY_REGISTERED);

    table::add(&mut election.voters, voter_address, VoterStatus { has_voted: false });
}

/// Casts a vote for a candidate in an election.
/// A voter can only cast one vote per election.
public entry fun cast_vote(
    election: &mut Election,
    candidate_id: ID,
    ctx: &TxContext,
) {
    let sender = tx_context::sender(ctx);

    // Basic election state checks
    assert!(election.is_active, E_ELECTION_NOT_ACTIVE);
    assert!(!election.is_finalized, E_ELECTION_FINALIZED);

    // Check if sender is a registered voter
    assert!(table::contains(&election.voters, sender), E_NOT_REGISTERED);

    // Check if sender has already voted
    let voter_status = table::borrow_mut(&mut election.voters, sender);
    assert!(!voter_status.has_voted, E_ALREADY_VOTED);

    // Check if the candidate exists in this election
    assert!(table::contains(&election.candidates, candidate_id), E_CANDIDATE_NOT_FOUND);

    // Increment vote count for the chosen candidate
    if (table::contains(&mut election.vote_counts, candidate_id)) {
        let count = table::borrow_mut(&mut election.vote_counts, candidate_id);
        *count = *count + 1;
    } else {
        // First vote for this candidate
        table::add(&mut election.vote_counts, candidate_id, 1);
    };

    // Mark voter as having voted
    voter_status.has_voted = true;
}

/// Finalizes an election, preventing further votes and making results immutable.
/// Only the election creator can finalize.
public entry fun finalize_election(
    election: &mut Election,
    ctx: &TxContext,
) {
    assert!(tx_context::sender(ctx) == election.creator, E_NOT_CREATOR);
    assert!(!election.is_finalized, E_ELECTION_FINALIZED); // Must not be finalized already
    // Assert that end_time has passed (requires Sui clock)
    // assert!(sui::clock::timestamp_ms(clock_object) >= election.end_time, E_ELECTION_NOT_ENDED);

    election.is_active = false;
    election.is_finalized = true;
}

// --- Public View Functions (Immutable, no `entry` keyword) ---

/// Gets the current vote counts for all candidates in an election.
/// Can be called by anyone.
public fun get_results(election: &Election): vector<VoteResult> {
    let results = vector::empty();
    let candidate_ids = table::keys(&election.candidates);

    while (vector::length(&candidate_ids) > 0) {
        let candidate_id = vector::pop_back(&mut candidate_ids);
        let candidate = table::borrow(&election.candidates, candidate_id);
        let votes = 0;
        if (table::contains(&election.vote_counts, candidate_id)) {
            votes = *table::borrow(&election.vote_counts, candidate_id);
        };
        vector::push_back(&mut results, VoteResult {
            candidate_name: string::clone(&candidate.name),
            votes,
        });
    };
    results
}

/// Gets the status of a specific voter in an election.
public fun get_voter_status(election: &Election, voter_address: address): VoterStatus {
    assert!(table::contains(&election.voters, voter_address), E_NOT_REGISTERED);
    *table::borrow(&election.voters, voter_address)
}

/// Gets election details.
public fun get_election_details(election: &Election): (String, String, u64, u64, bool, bool) {
    (
        string::clone(&election.name),
        string::clone(&election.description),
        election.start_time,
        election.end_time,
        election.is_active,
        election.is_finalized
    )
}

/// Gets a candidate's details.
public fun get_candidate_details(election: &Election, candidate_id: ID): String {
    assert!(table::contains(&election.candidates, candidate_id), E_CANDIDATE_NOT_FOUND);
    let candidate = table::borrow(&election.candidates, candidate_id);
    string::clone(&candidate.name)
}

}

6.5. Step 4: Write Tests (tests/voting_tests.move)

Testing is crucial for smart contracts. Sui Move provides a built-in testing framework.

// scorebox-voting/tests/voting_tests.move #[test_only] module scorebox_voting::voting_tests { use sui::test_scenario::{Self as test, Scenario}; use sui::object::{Self, ID}; use sui::string::{Self, String}; use sui::object_table::{Self, ObjectTable}; // For managing shared objects in tests use scorebox_voting::voting::{Self, Election, Candidate, VoterStatus, VoteResult};

// Helper function to create a String from a vector<u8>
fun new_string(bytes: vector<u8>): String {
    string::utf8(bytes)
}

#[test]
fun test_create_election() {
    let admin_address = @0xA;
    let scenario = test::begin(admin_address);

    // Test creating an election
    test::next_tx(&mut scenario, admin_address);
    {
        voting::create_election(
            new_string(b"My First Election"),
            new_string(b"A test election for demonstration"),
            1000, // start_time
            2000, // end_time
            test::ctx(&mut scenario)
        );
    };

    // Assert that an Election object was created and shared
    assert!(test::has_shared_object_with_id_and_type<Election>(&scenario, object::id_from_address(@0x0)), 0); // ID will be 0x0 for the first shared object in test
    // More specific checks can be done by getting the object back and checking its fields
    test::end(scenario);
}

#[test]
fun test_add_candidate_and_activate_election() {
    let admin_address = @0xA;
    let scenario = test::begin(admin_address);
    let election_id: ID = object::id_from_address(@0x0); // Placeholder, will get actual ID in test

    // Create election
    test::next_tx(&mut scenario, admin_address);
    {
        voting::create_election(
            new_string(b"Test Election"),
            new_string(b"Adding candidates and activating"),
            1000,
            2000,
            test::ctx(&mut scenario)
        );
        election_id = test::shared_object_id(&test::pop_shared_object<Election>(&mut scenario));
    };

    // Add candidates
    test::next_tx(&mut scenario, admin_address);
    {
        let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id);
        voting::add_candidate(election, new_string(b"Candidate A"), test::ctx(&mut scenario));
        voting::add_candidate(election, new_string(b"Candidate B"), test::ctx(&mut scenario));
    };

    // Activate election
    test::next_tx(&mut scenario, admin_address);
    {
        let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id);
        voting::activate_election(election, test::ctx(&mut scenario));
    };

    // Assert election is active
    test::next_tx(&mut scenario, admin_address);
    {
        let election = test::borrow_shared_object<Election>(&mut scenario, election_id);
        let (_, _, _, _, is_active, is_finalized) = voting::get_election_details(election);
        assert!(is_active, 0);
        assert!(!is_finalized, 0);
    };

    test::end(scenario);
}

#[test]
fun test_register_and_cast_vote() {
    let admin_address = @0xA;
    let voter_address = @0xB;
    let scenario = test::begin(admin_address);
    let election_id: ID = object::id_from_address(@0x0);
    let candidate_a_id: ID = object::id_from_address(@0x0); // Placeholder

    // Create election, add candidates, activate
    test::next_tx(&mut scenario, admin_address);
    {
        voting::create_election(new_string(b"Voting Test"), new_string(b""), 1000, 2000, test::ctx(&mut scenario));
        election_id = test::shared_object_id(&test::pop_shared_object<Election>(&mut scenario));
    };
    test::next_tx(&mut scenario, admin_address);
    {
        let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id);
        voting::add_candidate(election, new_string(b"Candidate A"), test::ctx(&mut scenario));
        candidate_a_id = table::keys(&election.candidates).pop_back(); // Get the ID of Candidate A
        voting::add_candidate(election, new_string(b"Candidate B"), test::ctx(&mut scenario));
    };
    test::next_tx(&mut scenario, admin_address);
    {
        let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id);
        voting::activate_election(election, test::ctx(&mut scenario));
    };

    // Register voter
    test::next_tx(&mut scenario, admin_address);
    {
        let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id);
        voting::register_voter(election, voter_address, test::ctx(&mut scenario));
    };

    // Cast vote
    test::next_tx(&mut scenario, voter_address);
    {
        let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id);
        voting::cast_vote(election, candidate_a_id, test::ctx(&mut scenario));
    };

    // Assert voter has voted
    test::next_tx(&mut scenario, voter_address);
    {
        let election = test::borrow_shared_object<Election>(&mut scenario, election_id);
        let voter_status = voting::get_voter_status(election, voter_address);
        assert!(voter_status.has_voted, 0);
    };

    // Assert vote count
    test::next_tx(&mut scenario, admin_address); // Can be any address for view function
    {
        let election = test::borrow_shared_object<Election>(&mut scenario, election_id);
        let results = voting::get_results(election);
        // Find Candidate A's result
        let i = 0;
        let found = false;
        while (i < vector::length(&results)) {
            let result = vector::borrow(&results, i);
            if (string::bytes(&result.candidate_name) == b"Candidate A") {
                assert!(result.votes == 1, 0);
                found = true;
                break;
            };
            i = i + 1;
        };
        assert!(found, 0); // Ensure Candidate A was found in results
    };

    test::end(scenario);
}

#[test]
#[expected_failure(abort_code = voting::E_ALREADY_VOTED)]
fun test_cannot_vote_twice() {
    let admin_address = @0xA;
    let voter_address = @0xB;
    let scenario = test::begin(admin_address);
    let election_id: ID = object::id_from_address(@0x0);
    let candidate_id: ID = object::id_from_address(@0x0);

    // Setup: Create, add candidate, activate, register voter, cast first vote
    test::next_tx(&mut scenario, admin_address);
    { voting::create_election(new_string(b"Double Vote Test"), new_string(b""), 1000, 2000, test::ctx(&mut scenario)); election_id = test::shared_object_id(&test::pop_shared_object<Election>(&mut scenario)); };
    test::next_tx(&mut scenario, admin_address);
    { let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id); voting::add_candidate(election, new_string(b"Candidate X"), test::ctx(&mut scenario)); candidate_id = table::keys(&election.candidates).pop_back(); };
    test::next_tx(&mut scenario, admin_address);
    { let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id); voting::activate_election(election, test::ctx(&mut scenario)); };
    test::next_tx(&mut scenario, admin_address);
    { let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id); voting::register_voter(election, voter_address, test::ctx(&mut scenario)); };
    test::next_tx(&mut scenario, voter_address);
    { let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id); voting::cast_vote(election, candidate_id, test::ctx(&mut scenario)); };

    // Attempt to vote a second time (expected to fail)
    test::next_tx(&mut scenario, voter_address);
    {
        let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id);
        voting::cast_vote(election, candidate_id, test::ctx(&mut scenario));
    };

    test::end(scenario);
}

#[test]
#[expected_failure(abort_code = voting::E_NOT_CREATOR)]
fun test_non_creator_cannot_finalize() {
    let admin_address = @0xA;
    let malicious_address = @0xC;
    let scenario = test::begin(admin_address);
    let election_id: ID = object::id_from_address(@0x0);

    // Setup: Create election
    test::next_tx(&mut scenario, admin_address);
    { voting::create_election(new_string(b"Finalize Test"), new_string(b""), 1000, 2000, test::ctx(&mut scenario)); election_id = test::shared_object_id(&test::pop_shared_object<Election>(&mut scenario)); };

    // Attempt to finalize from a non-creator address (expected to fail)
    test::next_tx(&mut scenario, malicious_address);
    {
        let election = test::borrow_shared_object_mut<Election>(&mut scenario, election_id);
        voting::finalize_election(election, test::ctx(&mut scenario));
    };

    test::end(scenario);
}

}

To run these tests:

sui move test

6.6. Step 5: Build the Project

Before deploying, build your Move package to check for syntax errors and compile it:

sui move build

6.7. Step 6: Deploy the Smart Contract (Publish)

Once your tests pass and the build is successful, you can publish your module to the Sui network (local, devnet, or testnet).

  • For local Sui network:

      1. Start a local Sui network: sui-node start (in a separate terminal).
      1. Get your active address: sui client active-address
      1. Publish your package:

    sui move publish --gas-budget 10000000

    The output will show your new package ID. This ID will replace 0x0 in your Move.toml's voting_system address definition if you used a named address.

  • For Devnet/Testnet:

      1. Ensure your Sui client is configured for Devnet/Testnet (sui client switch --env devnet).
      1. Ensure your active address has enough SUI tokens (faucet: sui client gas).
      1. Publish your package:

    sui move publish --gas-budget 10000000

    Again, note the package ID from the output.

6.8. Step 7: Interact with the Deployed Contract

After deployment, you can interact with your smart contract using the Sui CLI. You'll need the package ID (the address where your module is deployed) and the object ID of the Election you create.

  • Example Interaction Flow (using Sui CLI):

      1. Create an Election:

    sui client call --function create_election --module voting --package <YOUR_PACKAGE_ID> --args "My New Election" "This is a test election." 1716900000 1717000000 --gas-budget 10000000

    • Note the object ID of the created Election from the transaction output. Let's call it ELECTION_OBJECT_ID.

      1. Add a Candidate:

    sui client call --function add_candidate --module voting --package <YOUR_PACKAGE_ID> --args <ELECTION_OBJECT_ID> "Candidate Joe" --gas-budget 10000000

    • Note the object ID of the created Candidate from the transaction output. Let's call it CANDIDATE_JOE_ID.

      1. Activate Election:

    sui client call --function activate_election --module voting --package <YOUR_PACKAGE_ID> --args <ELECTION_OBJECT_ID> --gas-budget 10000000

      1. Register a Voter (as the creator):

    sui client call --function register_voter --module voting --package <YOUR_PACKAGE_ID> --args <ELECTION_OBJECT_ID> <VOTER_ADDRESS> --gas-budget 10000000

    • Replace <VOTER_ADDRESS> with the address of the person who will vote.

      1. Cast a Vote (as the voter):

        • Switch your active address to <VOTER_ADDRESS>: sui client switch --address <VOTER_ADDRESS>

        sui client call --function cast_vote --module voting --package <YOUR_PACKAGE_ID> --args <ELECTION_OBJECT_ID> <CANDIDATE_JOE_ID> --gas-budget 10000000

      1. Finalize Election (as the creator):

        • Switch back to creator address: sui client switch --address <CREATOR_ADDRESS>

        sui client call --function finalize_election --module voting --package <YOUR_PACKAGE_ID> --args <ELECTION_OBJECT_ID> --gas-budget 10000000

      1. Get Results (View Function): sui client call --function get_results --module voting --package <YOUR_PACKAGE_ID> --args <ELECTION_OBJECT_ID> --gas-budget 10000000 --view

        • The --view flag is important for read-only functions.

7. Security Considerations

  • Access Control (Crucial): All critical state-changing functions (create_election, add_candidate, activate_election, register_voter, finalize_election) strictly verify tx_context::sender(ctx) against the creator field of the Election object.

  • State Transitions: Assertions ensure operations only occur when the Election is in a valid state (e.g., cannot cast_vote if is_finalized is true, cannot add candidates if is_active).

  • One-Vote-Per-Voter: The voters table and has_voted flag within the Election aggregate are critical to enforcing this rule.

  • Input Validation: Basic validation for start_time and end_time is included. More comprehensive validation for all inputs is essential for production.

  • Sui Clock Integration: For precise time-based logic (e.g., activate_election only if current_time >= start_time, finalize_election only if current_time >= end_time), you must use Sui's sui::clock module and pass a &Clock object to your functions. This is omitted for simplicity in this basic example but is vital for real-world use.

  • Integer Overflow/Underflow: Move's integers are generally safe, but always be mindful of potential overflows if vote counts or other u64 values could exceed u64::MAX.

8. Future Enhancements

  • Dynamic Election Activation/Deactivation: Implement time-based activation and deactivation using the sui::clock object.

  • Weighted Voting: Allow voters to have different voting powers.

  • Decentralized Identity: Integrate with a decentralized identity solution for voter registration.

  • More Complex Voting Rules: Implement features like ranked-choice voting, multiple-choice options, or minimum participation thresholds.

  • Event Emitters: Emit events for key actions (e.g., VoteCast, ElectionFinalized) for off-chain indexing and UI updates.

  • Error Handling: Implement more granular error codes and messages.

  • Ownership of Candidates: Consider if Candidate objects should be owned by the Election or if they should be separate shared objects, depending on desired flexibility.

  • Emergency Pause/Unpause: Add a mechanism for the creator to pause/unpause an election in emergencies.

This domain driven design approach provides a solid, secure, and extensible foundation for our basic voting system on Sui Move, complete with a clear project structure and step-by-step implementation guide.

About

Sui based voting system using Move

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published