Soroban Series 3: Building a Soroban Voting Contract
Published: 2023-12-22 09:17:06

## 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] (https://github.com/TosinShada/SorobanVote)
Demo: [Link] (https://soroban-vote.vercel.app)
Video: [Link] (https://youtu.be/FsD_Zx0y6tU)

### Prerequisites

Before we begin, make sure you have the following:

- Rust installed (check [Rust Installation Guide](https://www.rust-lang.org/tools/install))
- A basic understanding of Rust programming language
- Familiarity with Soroban smart contracts and blockchain concepts (check [Soroban Starter Guide](https://soroban.stellar.org/docs/getting-started/setup)) 

## Setting Up the Project

1. **Create a New Rust Project:**

   Create a new Rust project using the following command:

   ```bash
   cargo new soroban_voting_contract
   ```

2. **Navigate to the Project Directory:**

   Navigate to the project directory:

   ```bash
   cd soroban_voting_contract
   ```

3. **Add Dependencies:**

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

   ```toml
   [workspace.dependencies]
   soroban-sdk = "20.0.3"
   soroban-token-sdk = "20.0.3"
   ```

4. **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

``` rust
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);
    }
```

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

``` rust
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
    }
```

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

``` rust
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);
    }
```

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

``` rust
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;
    }
```

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

``` rust 
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);
    }
```

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:

   ```bash
   cargo build --release --target wasm32-unknown-unknown
   ```

2. **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](https://soroban.stellar.org/docs) for more details and advanced features. Happy coding!