DEV Community

Cover image for Soroban Series 3: Building a Soroban Voting Contract
Shada Tosin
Shada Tosin

Posted on

Soroban Series 3: Building a Soroban Voting Contract

Introduction

Welcome to this comprehensive tutorial on building a Soroban-based voting contract. Soroban is a lightweight smart contract framework designed for simplicity and efficiency. In this tutorial, we'll go through the step-by-step implementation of a basic voting contract using the Soroban framework.

Source Code: Link
Demo: Link
Video: Link

Prerequisites

Before we begin, make sure you have the following:

Setting Up the Project

  1. Create a New Rust Project:

Create a new Rust project using the following command:

   cargo new soroban_voting_contract
Enter fullscreen mode Exit fullscreen mode
  1. Navigate to the Project Directory:

Navigate to the project directory:

   cd soroban_voting_contract
Enter fullscreen mode Exit fullscreen mode
  1. Add Dependencies:

Open the Cargo.toml file and add the following dependencies:

   [workspace.dependencies]
   soroban-sdk = "20.0.3"
   soroban-token-sdk = "20.0.3"
Enter fullscreen mode Exit fullscreen mode
  1. Create Contract Code:

Create a new file called lib.rs in the src directory and copy the provided Soroban contract code into it.

Understanding the Soroban Voting Contract

Let's dive deeper into the key components of the Soroban voting contract:

1. Storage Management

The contract leverages Soroban's storage capabilities for efficient data management. It defines data keys such as NextProposalId and Proposals to organize the storage of proposal information.

2. Initialization Function

pub fn initialize(e: Env) {
        assert!(
            !e.storage().instance().has(&DataKey::NextProposalId),
            "already initialized"
        );
        let initial_proposal_id: u128 = 1;
        e.storage()
            .instance()
            .set(&DataKey::NextProposalId, &initial_proposal_id);
        e.storage()
            .instance()
            .extend_ttl(INSTANCE_LIFETIME_THRESHOLD, INSTANCE_BUMP_AMOUNT);
    }
Enter fullscreen mode Exit fullscreen mode

The initialize function sets up the initial state of the contract. It checks whether the contract is already initialized and, if not, initializes the NextProposalId and extends the contract's lifetime.

3. Proposal Creation

pub fn create_proposal(
        e: Env,
        sender: Address,
        name: String,
        description: String,
        goal: u128,
    ) -> u128 {
        sender.require_auth();
        let proposal_id = get_next_proposal_id(&e);

        let created_at = get_ledger_timestamp(&e);
        let proposal = Proposal {
            id: proposal_id,
            name: name.clone(),
            description: description.clone(),
            goal,
            created_at,
            is_active: true,
            status: ProposalStatus::Active,
            last_voted_at: 0,
            vote_count: 0,
            created_by: sender.clone(),
        };

        e.storage()
            .persistent()
            .set(&DataKey::Proposals(proposal_id), &proposal);
        e.storage().persistent().extend_ttl(
            &DataKey::Proposals(proposal_id),
            LIFETIME_THRESHOLD,
            BUMP_AMOUNT,
        );

        set_proposal_by_user(&e, &proposal_id);

        // emit events
        events::proposal_created(&e, proposal_id, sender, name, created_at);

        proposal_id
    }
Enter fullscreen mode Exit fullscreen mode

The create_proposal function allows users to create a new voting proposal. It performs validation checks, including verifying the sender's authorization, generates a unique proposal ID, and stores the proposal details in persistent storage.

4. Voting Mechanism

pub fn vote(e: Env, proposal_id: u128, voter: Address) {
        voter.require_auth();
        let mut proposal = get_proposal(&e, &proposal_id);
        assert!(proposal.is_active, "proposal is not active");
        assert!(
            proposal.status == ProposalStatus::Active,
            "proposal is not active"
        );
        proposal.last_voted_at = get_ledger_timestamp(&e);
        proposal.vote_count += 1;
        e.storage()
            .persistent()
            .set(&DataKey::Proposals(proposal_id), &proposal);
        e.storage().persistent().extend_ttl(
            &DataKey::Proposals(proposal_id),
            LIFETIME_THRESHOLD,
            BUMP_AMOUNT,
        );

        // emit events
        events::proposal_voted(&e, proposal_id, voter);
    }
Enter fullscreen mode Exit fullscreen mode

The vote function enables users to cast their votes for a specific proposal. It checks the proposal's status, updates the vote count, and emits relevant events to notify other stakeholders.

5. Proposal Status Management

pub fn status(e: Env, proposal_id: u128) -> ProposalStatus {
        let mut proposal = get_proposal(&e, &proposal_id);

        if proposal.status != ProposalStatus::Active {
            return proposal.status;
        }

        if proposal.vote_count >= proposal.goal {
            proposal.is_active = false;
            proposal.status = ProposalStatus::Ended;

            e.storage()
                .persistent()
                .set(&DataKey::Proposals(proposal_id), &proposal);
            e.storage().persistent().extend_ttl(
                &DataKey::Proposals(proposal_id),
                LIFETIME_THRESHOLD,
                BUMP_AMOUNT,
            );
            // emit events
            events::proposal_reached_target(&e, proposal_id, proposal.goal);
            return proposal.status;
        }

        return ProposalStatus::Active;
    }
Enter fullscreen mode Exit fullscreen mode

The status function checks the status of a proposal and handles its lifecycle. If the proposal reaches its goal, it is marked as ended, and events are emitted to inform the relevant parties.

6. Proposal Cancellation

pub fn cancel_proposal(e: Env, sender: Address, proposal_id: u128) {
        sender.require_auth();
        let mut proposal = get_proposal(&e, &proposal_id);
        assert!(
            proposal.created_by == sender,
            "only the creator can cancel the proposal"
        );
        assert!(proposal.is_active, "proposal is not active");
        assert!(
            proposal.status == ProposalStatus::Active,
            "proposal is not active"
        );
        proposal.is_active = false;
        proposal.status = ProposalStatus::Cancelled;
        e.storage()
            .persistent()
            .set(&DataKey::Proposals(proposal_id), &proposal);
        e.storage().persistent().extend_ttl(
            &DataKey::Proposals(proposal_id),
            LIFETIME_THRESHOLD,
            BUMP_AMOUNT,
        );

        events::proposal_cancelled(&e, proposal_id);
    }
Enter fullscreen mode Exit fullscreen mode

The cancel_proposal function allows the creator to cancel an active proposal. It checks the authorization of the sender, sets the proposal as inactive, and updates its status accordingly.

7. Utility Functions

Utility functions such as get_proposal and get_proposals are provided to retrieve specific proposals or all proposals associated with a user.

Compiling and Deploying the Contract

  1. Compile the Contract:

Open a terminal and navigate to the project directory. Compile the contract using the following command:

   cargo build --release --target wasm32-unknown-unknown
Enter fullscreen mode Exit fullscreen mode
  1. Deployment:

Deploy the compiled contract on Stellar Testnet or Futurenet. Refer to the documentation of the specific platform for detailed deployment instructions.

Advanced Features and Considerations

Now that you've built a basic Soroban voting contract, consider exploring advanced features and optimizations:

  • Gas Efficiency: Optimize the contract for gas efficiency to reduce transaction costs.
  • Security Audits: Perform security audits to identify and address potential vulnerabilities.
  • Event Handling: Enhance event handling mechanisms for better communication with external applications.
  • Integration with Oracles: Explore integration with Oracles to bring external data into the contract.

Conclusion

Congratulations! You've successfully created a Soroban-based voting contract. This tutorial serves as a foundation for building more complex smart contracts using Soroban. Feel free to explore and extend the functionality based on your requirements. Check the Soroban documentation for more details and advanced features. Happy coding!

Top comments (0)