Drips Wave Program — Fix issues, get merged, earn rewards at drips.network/wave/stellar
Welcome, and thank you for contributing to StellarGrants Protocol — a decentralized, milestone-based grant management system built with Rust and Soroban on the Stellar blockchain. This guide covers everything you need to go from zero to merged PR.
- Code of Conduct
- Getting Started
- How the Drips Wave Works
- Finding an Issue
- Claiming an Issue
- Branching Strategy
- Commit Message Format
- Pull Request Process
- Code Style & Linting
- Writing Tests
- Contract-Specific Guidelines
- Documentation Guidelines
- Issue Labels Explained
- Getting Help
This project follows the Contributor Covenant Code of Conduct. By participating, you agree to uphold a welcoming and respectful environment. Report violations to the maintainer via GitHub Issues or Discord.
Make sure you have the following installed before contributing:
| Tool | Version | Purpose |
|---|---|---|
| Rust | >= 1.78 |
Smart contract language |
| stellar CLI | Latest | Deploy & invoke contracts |
wasm32v1-none target |
— | Compile contracts to WASM |
| Node.js | >= 18 |
TypeScript client examples |
| Git | Any | Version control |
Install the WASM target:
rustup target add wasm32v1-noneInstall the Stellar CLI:
cargo install --locked stellar-cli --features opt# 1. Fork the repo via GitHub UI, then:
git clone https://github.com/StellarGrant/StellarGrant-Contracts
cd StellarGrant-Contracts
# 2. Add upstream remote
git remote add upstream https://github.com/StellarGrant/StellarGrant-Contracts# Build the contract
make build
# Run all tests
make test
# Run linter
make lint
# Format code
make fmt
# Deploy to testnet
make deploy-testnetOr using Cargo directly:
cargo build --target wasm32v1-none --release
cargo test
cargo clippy -- -D warnings
cargo fmt --checkThe Stellar Wave Program on Drips is a monthly contribution sprint where:
- Maintainers post scoped GitHub issues with clear acceptance criteria
- Contributors claim issues, submit PRs, and earn Wave Points for merged work
- Points translate to real rewards distributed at the end of each Wave cycle
- A new Wave runs roughly every month — watch for announcements at drips.network/wave/stellar
💡 Tip: Subscribe to the Drips Wave newsletter and join their Discord to be notified when new Waves open.
To be eligible for Wave rewards:
- Your PR must be merged before the Wave closes
- The issue must carry the
drips-wavelabel - Your GitHub account must be registered on Drips
Start here:
- New to Soroban? Filter by
good first issue— these are well-scoped with step-by-step guidance - Know Rust? Look for
coreorsecuritylabels - Love testing? Filter by
testing - DevEx contributor? Check
documentationanddevex
Browse the full issue list and sort by Newest or Most commented to find active discussions.
Before writing a single line of code:
-
Check for existing assignees — if someone is already assigned, don't duplicate work
-
Comment on the issue with something like:
I'd like to work on this. My approach: - [brief plan of what you'll do] - ETA: [rough timeline] -
Wait for maintainer acknowledgment — a maintainer will assign the issue to you (usually within 24–48h)
-
Unassigned after 5 days? If an assigned contributor goes silent, a maintainer may reassign — ping the issue thread first
⚠️ Do not open a PR for an unclaimed issue — it may be rejected if another contributor is already working on it.
Always branch off main:
git checkout main
git pull upstream main
git checkout -b feature/issue-42-milestone-approve-voteBranch naming convention:
| Type | Pattern | Example |
|---|---|---|
| New feature | feature/issue-<N>-<short-name> |
feature/issue-6-milestone-approve |
| Bug fix | fix/issue-<N>-<short-name> |
fix/issue-33-overflow-check |
| Documentation | docs/issue-<N>-<short-name> |
docs/issue-38-readme |
| Tooling/CI | chore/issue-<N>-<short-name> |
chore/issue-49-github-actions |
| Tests | test/issue-<N>-<short-name> |
test/issue-23-grant-create-tests |
We follow the Conventional Commits specification:
<type>(<scope>): <short summary>
[optional body]
[optional footer: Closes #N]
Types:
| Type | When to use |
|---|---|
feat |
New contract function or feature |
fix |
Bug fix |
test |
Adding or updating tests |
docs |
Documentation only |
chore |
Tooling, CI, build changes |
refactor |
Code restructure without behavior change |
perf |
Performance improvement |
security |
Security fix or hardening |
Examples:
feat(contract): implement milestone_approve() with DAO vote logic
- Add reviewer vote storage with Map<Address, bool>
- Enforce quorum threshold before triggering payout
- Emit MilestoneApproved event on success
Closes #6test(escrow): add fuzz tests for grant_fund() with random inputs
Closes #27docs(readme): add quick start and deployment guide
Closes #38✅ Keep the subject line under 72 characters. Use the body for why, not what.
-
Push your branch to your fork:
git push origin feature/issue-6-milestone-approve
-
Open a PR against
mainon the upstream repo -
Use this PR title format:
feat(contract): implement milestone_approve() (#6) -
Fill out the PR template (auto-populated when you open a PR):
## Summary Brief description of the changes. ## Related Issue Closes #6 ## Changes Made - Added `milestone_approve()` function in `lib.rs` - Added vote storage accessors in `storage/helpers.rs` - Emits `MilestoneApproved` event ## Testing - [ ] Unit tests added/updated - [ ] `cargo test` passes locally - [ ] `cargo clippy` passes with no warnings - [ ] `cargo fmt` applied ## Notes for Reviewer Any context, tradeoffs, or open questions.
-
CI must pass — all checks (build, test, lint, fmt) must be green before review
-
Request review — tag
@maintainerif no review after 48h -
Respond to feedback — address comments with code or explanation; re-request review after updates
-
Squash on merge — maintainer will squash commits; your branch can have WIP commits
We enforce consistent style via CI. Run these before every push:
# Format
cargo fmt
# Lint (must be warning-free)
cargo clippy -- -D warningsRust style rules:
- Use
snake_casefor functions and variables - Use
PascalCasefor types, structs, and enums - Use
SCREAMING_SNAKE_CASEfor constants - Prefer explicit error types over
unwrap()— useResult<T, ContractError> - Add
/// rustdoccomments to every public function - Keep functions under 50 lines — extract helpers when needed
- No dead code (
#[allow(dead_code)]requires maintainer approval)
Soroban-specific rules:
- Always call
env.storage()through theStoragewrapper instorage/helpers.rs - Use
require_auth()at the top of every state-changing function - Use
checked_add/checked_subfor all balance math — never raw arithmetic - Emit events via
env.events().publish()for all state transitions - Use the typed
DataKeyenum fromstorage/keys.rsfor storage keys — never rawSymbols inlib.rs
All PRs touching contract logic must include tests. We target ≥ 80% coverage.
Test file location: contracts/stellar-grants/src/test.rs
Basic test structure:
#[cfg(test)]
mod tests {
use super::*;
use soroban_sdk::{testutils::Address as _, Address, Env};
#[test]
fn test_grant_create_success() {
let env = Env::default();
env.mock_all_auths();
let contract_id = env.register_contract(None, StellarGrantsContract);
let client = StellarGrantsContractClient::new(&env, &contract_id);
let owner = Address::generate(&env);
let grant_id = client.grant_create(&owner, &String::from_str(&env, "My Grant"), &1000, &100, &3);
assert_eq!(grant_id, 0);
}
}Test coverage expectations by issue type:
| Issue type | Minimum tests required |
|---|---|
| New contract function | Happy path + at least 2 edge/error cases |
| Bug fix | Regression test that would have caught the bug |
| Security issue | Unauthorized caller test + boundary test |
| Refactor | Existing tests must still pass; no new tests required |
Run coverage locally:
cargo install cargo-llvm-cov
cargo llvm-cov --html
open target/llvm-cov/html/index.htmlStorage is organized as a storage/ module, not a single file:
storage/keys.rs— the typed#[contracttype]key enumsstorage/helpers.rs— theStoragewrapper with typed accessorsstorage/mod.rs— re-exports the public storage surface
All storage keys must be defined as variants of the typed DataKey enum in
storage/keys.rs — never as raw Symbols inlined in lib.rs. DataKey groups
related keys under per-domain sub-enums (GrantKey, MilestoneKey, EscrowKey,
UserKey, …), each annotated with #[contracttype] so Soroban derives a stable
XDR encoding:
// storage/keys.rs
#[contracttype]
#[derive(Clone)]
pub enum GrantKey {
Data(u64),
Counter,
// ...
}
#[contracttype]
#[derive(Clone)]
pub enum DataKey {
Grant(GrantKey),
Milestone(MilestoneKey),
// ... protocol singletons, per-address keys, etc.
Admin,
}// ✅ Correct — typed key via the Storage wrapper / DataKey enum
env.storage().persistent().get(&DataKey::Grant(GrantKey::Data(grant_id)))
// ❌ Wrong — raw Symbol string in lib.rs
env.storage().persistent().get(&Symbol::new(env, "grant"))Every function that mutates state must begin with an auth check:
pub fn milestone_approve(env: Env, grant_id: u64, milestone_idx: u32, reviewer: Address) {
reviewer.require_auth(); // ← must be first
// ...
}Emit a structured event for every state change:
env.events().publish(
(Symbol::new(&env, "MilestoneApproved"), grant_id),
(milestone_idx, reviewer.clone()),
);Use the shared ContractError enum defined in types.rs:
pub enum ContractError {
GrantNotFound = 1,
Unauthorized = 2,
MilestoneAlreadyApproved = 3,
QuorumNotReached = 4,
DeadlinePassed = 5,
// ...
}Return Result<T, ContractError> — never panic!() in production paths.
- Every public function needs a
///rustdoc comment with# Arguments,# Returns, and# Errorssections - Update
ARCHITECTURE.mdif your PR changes the storage layout or auth model - Update
README.mdif your PR adds or changes a CLI command or contract function signature - New environment variables or config values must be documented in
.env.example
Rustdoc example:
/// Approves a submitted milestone after reaching reviewer quorum.
///
/// # Arguments
/// * `env` - The Soroban environment
/// * `grant_id` - The ID of the grant containing the milestone
/// * `milestone_idx` - Zero-based index of the milestone to approve
/// * `reviewer` - Address of the reviewer casting the vote
///
/// # Returns
/// `true` if the milestone is fully approved and payout was triggered, `false` if vote recorded but quorum not yet reached.
///
/// # Errors
/// * `ContractError::GrantNotFound` - if `grant_id` doesn't exist
/// * `ContractError::Unauthorized` - if `reviewer` is not in the reviewer list
/// * `ContractError::MilestoneAlreadyApproved` - if milestone was already approved
pub fn milestone_approve(env: Env, grant_id: u64, milestone_idx: u32, reviewer: Address) -> Result<bool, ContractError> {| Label | Meaning |
|---|---|
good first issue |
Well-scoped, beginner-friendly — ideal for your first Soroban contribution |
core |
Core contract logic: grant lifecycle, escrow, milestone state machine |
security |
Security-critical — auth, overflow, reentrancy, access control |
testing |
Unit, integration, or fuzz tests |
documentation |
README, rustdoc, guides, architecture docs |
tooling |
CI/CD, build scripts, CLI tooling |
performance |
WASM size, instruction count, fee optimization |
devex |
TypeScript bindings, example clients, CLI UX |
enhancement |
New feature or improvement to existing functionality |
bug |
Something broken or incorrect |
drips-wave |
Eligible for Drips Wave reward points |
Stuck? Here's where to get support:
- GitHub Discussions — ask questions, propose ideas, share feedback
- Issue comments — ask clarifying questions directly on the issue you're working on
- Drips Discord —
#stellar-wavechannel at discord.gg/drips - Stellar Developer Discord — discord.gg/stellardev for Soroban/SDK questions
Please don't DM maintainers for support — use public channels so others can benefit from the answer too.
Every contribution — whether it's a one-line doc fix or a full DAO voting implementation — makes StellarGrants Protocol better. We're building open infrastructure for the Stellar ecosystem, one merged PR at a time.
Fix. Merge. Earn. 🌊