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:
- Rust installed (check Rust Installation Guide)
- A basic understanding of Rust programming language
- Familiarity with Soroban smart contracts and blockchain concepts (check Soroban Starter Guide)
Setting Up the Project
- Create a New Rust Project:
Create a new Rust project using the following command:
cargo new soroban_voting_contract
- Navigate to the Project Directory:
Navigate to the project directory:
cd soroban_voting_contract
- 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"
- 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);
}
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
}
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);
}
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;
}
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);
}
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
- 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
- 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)