Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,25 @@ jobs:
targets: wasm32-unknown-unknown
- run: cargo test --features testutils
- run: cargo build --target wasm32-unknown-unknown --release

coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
targets: wasm32-unknown-unknown
- name: Install cargo-llvm-cov
run: cargo install cargo-llvm-cov --locked
- name: Generate code coverage
run: cargo llvm-cov --features testutils --lcov --output-path lcov.info --fail-under-lines 80
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
token: ${{ secrets.CODECOV_TOKEN }}
files: lcov.info
# Don't fail the pipeline if the upload fails (e.g. token not yet configured).
# The coverage threshold is enforced by `--fail-under-lines 80` above.
fail_ci_if_error: false
verbose: true
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ target_local/
.DS_Store
Thumbs.db

# =========================
# Coverage output (generated by `cargo llvm-cov` in CI)
# =========================
/lcov.info

# =========================
# Logs & temp
# =========================
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).

### Fixed

- Community voting is now count-based (1 address = 1 vote): `cast_vote` tallies approval/rejection vote counts instead of token-weighted balances, and `verify_with_votes` derives the approval percentage from those counts. This closes the flash-loan voting attack (#448), where an attacker could temporarily borrow tokens to inflate their voting weight. The `ApproveWeight`/`RejectWeight` storage keys are retained (unused) for ledger-XDR compatibility. The `balance` field in `campaign_vote_cast` events is now informational only.

- `cancel_campaign` now rejects with `GoalMetCancellationNotAllowed` when `amount_raised >= funding_goal` and funds have not yet been withdrawn, preventing rug-pull-adjacent behaviour where a creator could cancel after reaching the goal and force all contributors to self-serve refunds (#164).

- `update_campaign_description` now blocks edits once `amount_raised > 0`, preventing bait-and-switch after contributions (#166).
Expand Down
8 changes: 6 additions & 2 deletions EVENT_PAYLOADS.md
Original file line number Diff line number Diff line change
Expand Up @@ -459,8 +459,12 @@ Every `publish(...)` call in the contract, with its topics, data shape, and the
| Field | Value |
|---------|------------------------------------------------------------|
| Topics | `("campaign_vote_cast", campaign_id: u32, voter: Address)` |
| Data | `(approve: bool, balance: i128, weight: i128)` |
| Source | `voting.rs:100` — `cast_vote()` |
| Data | `(approve: bool, balance: i128)` |
| Source | `voting.rs:90` — `cast_vote()` |

> **Note:** After the #448 flash-loan fix, voting is count-based (1 address = 1 vote).
> The `balance` field remains in the event payload for informational/indexing purposes
> but no longer affects the approval-threshold calculation.

---

Expand Down
35 changes: 0 additions & 35 deletions src/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -509,38 +509,3 @@ pub(crate) fn resume_campaign(env: &Env, campaign_id: u32, caller: Address) -> R

Ok(())
}

use soroban_sdk::{contractimpl, Address, Env, String};
use crate::errors::Error;

#[contractimpl]
impl ProofOfHeartContract {
/// Sets or updates the maximum funding goal cap for a specific campaign category.
pub fn set_category_max_goal_cap(
env: Env,
admin: Address,
category: String,
max_goal: i128,
) -> Result<(), Error> {
admin.require_auth();

// Verify admin permissions (assumes admin check helper exists)
Self::verify_admin(&env, &admin)?;

let cap_key = DataKey::CategoryMaxGoalCap(category.clone());
env.storage().persistent().set(&cap_key, &max_goal);

env.events().publish(
(Symbol::new(&env, "category_cap_updated"), category),
max_goal,
);

Ok(())
}

/// Retrieves the maximum funding goal cap for a given category, if defined.
pub fn get_category_max_goal_cap(env: Env, category: String) -> Option<i128> {
let cap_key = DataKey::CategoryMaxGoalCap(category);
env.storage().persistent().get(&cap_key)
}
}
7 changes: 0 additions & 7 deletions src/campaigns.rs

This file was deleted.

28 changes: 0 additions & 28 deletions src/clients.rs

This file was deleted.

32 changes: 0 additions & 32 deletions src/events.ts

This file was deleted.

30 changes: 0 additions & 30 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -647,33 +647,3 @@ impl ProofOfHeart {

#[cfg(test)]
mod tests;

use soroban_sdk::{contract, contractimpl, Env, String, Vec};

#[contract]
pub struct ProofOfHeartContract;

#[contractimpl]
impl ProofOfHeartContract {
/// Lists active campaigns, optionally filtered by a specific tag string.
pub fn list_active_campaigns(env: Env, tag_filter: Option<String>) -> Vec<Campaign> {
let all_campaigns: Vec<Campaign> = env
.storage()
.instance()
.get(&DataKey::Campaigns)
.unwrap_or(Vec::new(&env));

match tag_filter {
Some(filter_tag) => {
let mut filtered = Vec::new(&env);
for campaign in all_campaigns.iter() {
if campaign.tags.contains(&filter_tag) {
filtered.push_back(campaign);
}
}
filtered
}
None => all_campaigns,
}
}
}
36 changes: 0 additions & 36 deletions src/proof_of_heart/src/admin.rs

This file was deleted.

33 changes: 0 additions & 33 deletions src/proof_of_heart/src/voting.rs

This file was deleted.

4 changes: 4 additions & 0 deletions src/storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -499,23 +499,27 @@ pub fn set_reject_votes(env: &Env, campaign_id: u32, count: u32) {
// ── Vote weights (token-weighted) ─────────────────────────────────────────────

/// Returns the total approval token-weight for a campaign.
#[expect(dead_code)]
pub fn get_approve_weight(env: &Env, campaign_id: u32) -> i128 {
let key = VotingKey::ApproveWeight(campaign_id);
env.storage().persistent().get(&key).unwrap_or(0)
}

/// Stores the total approval token-weight for a campaign and extends its TTL.
#[expect(dead_code)]
pub fn set_approve_weight(env: &Env, campaign_id: u32, weight: i128) {
persistent_set!(env, VotingKey::ApproveWeight(campaign_id), &weight);
}

/// Returns the total rejection token-weight for a campaign.
#[expect(dead_code)]
pub fn get_reject_weight(env: &Env, campaign_id: u32) -> i128 {
let key = VotingKey::RejectWeight(campaign_id);
env.storage().persistent().get(&key).unwrap_or(0)
}

/// Stores the total rejection token-weight for a campaign and extends its TTL.
#[expect(dead_code)]
pub fn set_reject_weight(env: &Env, campaign_id: u32, weight: i128) {
persistent_set!(env, VotingKey::RejectWeight(campaign_id), &weight);
}
Expand Down
23 changes: 0 additions & 23 deletions src/test.rs

This file was deleted.

16 changes: 10 additions & 6 deletions src/tests/test_campaign_update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ fn test_update_campaign_emits_title_and_description() {

let events = env.events().all();
let last_event = events.last().unwrap();
let payload: (String, String) = soroban_sdk::FromVal::from_val(&env, &last_event.2);
// Event payload is (old_title, old_description, new_title, new_description)
let payload: (String, String, String, String) =
soroban_sdk::FromVal::from_val(&env, &last_event.2);

assert_eq!(payload.0, new_title);
assert_eq!(payload.1, new_desc);
assert_eq!(payload.2, new_title);
assert_eq!(payload.3, new_desc);
}

#[test]
Expand Down Expand Up @@ -94,9 +96,11 @@ fn test_update_campaign_event_tracks_latest_description() {

let events = env.events().all();
let last_event = events.last().unwrap();
let payload: (String, String) = soroban_sdk::FromVal::from_val(&env, &last_event.2);
assert_eq!(payload.0, String::from_str(&env, "Title V3"));
assert_eq!(payload.1, String::from_str(&env, "Description V3"));
// Event payload is (old_title, old_description, new_title, new_description)
let payload: (String, String, String, String) =
soroban_sdk::FromVal::from_val(&env, &last_event.2);
assert_eq!(payload.2, String::from_str(&env, "Title V3"));
assert_eq!(payload.3, String::from_str(&env, "Description V3"));
}

#[test]
Expand Down
Loading
Loading