From d2fc0810bac04d5ff1a33127a7807795cce6117c Mon Sep 17 00:00:00 2001 From: bbkenny Date: Fri, 31 Jul 2026 22:01:51 +0100 Subject: [PATCH 1/4] fix: guarantee wasm build reproducibility via dockerized make target Added a Makefile utilizing the official stellar/rs-soroban-sdk docker image to ensure the WASM bytecode generated is deterministic. This resolves the security concern where deployed contracts could not be independently verified against source. Also updated docs, changelog, and removed a stray unlinked file to clear compilation blockers. --- CHANGELOG.md | 1 + CONTRIBUTING.md | 9 ++++++--- Makefile | 18 ++++++++++++++++++ README.md | 7 +++++-- src/admin.rs | 4 ++-- src/campaigns.rs | 7 ------- src/lib.rs | 2 +- 7 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 Makefile delete mode 100644 src/campaigns.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 29a66f00..b291c936 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ### Infrastructure +- Added a `Makefile` with a `build-docker` target utilizing the `stellar/rs-soroban-sdk` image to guarantee WASM binary reproducibility, allowing anyone to verify that the deployed on-chain bytecode matches the source (#533). - Resolved pre-existing CI debt surfaced by the `fmt` and `clippy` gates added in #403: test fixture missing bindings restored in `src/test.rs` and `src/tests/test_init.rs`, `result` double-move fixed in `src/tests/test_admin.rs`, `cargo fmt --all` drift cleared across `src/issues_test.rs` and `src/lib.rs`, and clippy lints addressed (`manual_div_ceil` in `src/lib.rs`; `dead_code` suppressed on deferred storage helpers pending the DataKey audit in #409). All three CI jobs (`test`, `fmt`, `clippy`) now exit 0 on a clean checkout (#418). ### Refactored diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a3772c82..d8ed6ea5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -43,8 +43,11 @@ git remote add upstream https://github.com/Iris-IV/ProofOfHeart-stellar.git > **Heads up:** The first `cargo build` downloads and compiles all Rust dependencies. This can take **10–20 minutes** and use **1–2 GB** of disk space. Subsequent builds are much faster. ```bash -# Build WASM -cargo build --target wasm32-unknown-unknown --release +# Build WASM (Local development) +stellar contract build + +# Build reproducible WASM (Required for production deployment) +make build-docker # Run tests cargo test --features testutils @@ -60,7 +63,7 @@ CI runs these checks on every PR. Run locally before pushing: cargo fmt --check cargo clippy --all-targets --features testutils -- -D warnings cargo test --features testutils -cargo build --target wasm32-unknown-unknown --release +stellar contract build ``` All four must pass. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..26f1887a --- /dev/null +++ b/Makefile @@ -0,0 +1,18 @@ +default: build + +all: test + +build: + stellar contract build + +build-docker: + docker run --rm -v $(PWD):/workspace -w /workspace stellar/rs-soroban-sdk:20.1.0 stellar contract build + +test: + cargo test --features testutils + +fmt: + cargo fmt --all + +clean: + cargo clean diff --git a/README.md b/README.md index a1f14c2b..03fc7bdf 100644 --- a/README.md +++ b/README.md @@ -139,8 +139,11 @@ This repository contains the **Soroban smart contract** that powers the on-chain git clone https://github.com/Iris-IV/ProofOfHeart-stellar.git cd ProofOfHeart-stellar -# Build the contract -cargo build --target wasm32-unknown-unknown --release +# Build the contract for local development +stellar contract build + +# Build a deterministic, reproducible WASM binary for deployment/verification +make build-docker ``` ### Test diff --git a/src/admin.rs b/src/admin.rs index e6e6c3f8..abb429a9 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -510,8 +510,8 @@ 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; +use soroban_sdk::{contractimpl, Address, Env, String}; #[contractimpl] impl ProofOfHeartContract { @@ -543,4 +543,4 @@ impl ProofOfHeartContract { let cap_key = DataKey::CategoryMaxGoalCap(category); env.storage().persistent().get(&cap_key) } -} \ No newline at end of file +} diff --git a/src/campaigns.rs b/src/campaigns.rs deleted file mode 100644 index 980cafb1..00000000 --- a/src/campaigns.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Inside create_campaign function or validation module -let category_cap_key = DataKey::CategoryMaxGoalCap(campaign_category.clone()); -if let Some(max_cap) = env.storage().persistent().get::(&category_cap_key) { - if funding_goal > max_cap { - return Err(Error::FundingGoalExceedsCategoryCap); - } -} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index d3c21240..0957217a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -676,4 +676,4 @@ impl ProofOfHeartContract { None => all_campaigns, } } -} \ No newline at end of file +} From a8d65f4eaf14eafd8285e8240bb601dd1e8c6812 Mon Sep 17 00:00:00 2001 From: bbkenny Date: Fri, 31 Jul 2026 22:57:01 +0100 Subject: [PATCH 2/4] fix: remove stray unused struct and functions causing CI failure --- src/admin.rs | 34 ---------------------------------- src/lib.rs | 29 ----------------------------- 2 files changed, 63 deletions(-) diff --git a/src/admin.rs b/src/admin.rs index abb429a9..dbedf0c5 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -510,37 +510,3 @@ pub(crate) fn resume_campaign(env: &Env, campaign_id: u32, caller: Address) -> R Ok(()) } -use crate::errors::Error; -use soroban_sdk::{contractimpl, Address, Env, String}; - -#[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 { - let cap_key = DataKey::CategoryMaxGoalCap(category); - env.storage().persistent().get(&cap_key) - } -} diff --git a/src/lib.rs b/src/lib.rs index 0957217a..f7785fb9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -648,32 +648,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) -> Vec { - let all_campaigns: Vec = 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, - } - } -} From 3d807ce6c7ec13aa318bc2c8db655349a1236373 Mon Sep 17 00:00:00 2001 From: bbkenny Date: Fri, 31 Jul 2026 23:46:41 +0100 Subject: [PATCH 3/4] fix: resolve failing CI formatting and upstream metadata test drift --- src/admin.rs | 1 - src/lib.rs | 1 - src/tests/test_campaign_update.rs | 16 ++++++++++------ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/admin.rs b/src/admin.rs index dbedf0c5..0256f571 100644 --- a/src/admin.rs +++ b/src/admin.rs @@ -509,4 +509,3 @@ pub(crate) fn resume_campaign(env: &Env, campaign_id: u32, caller: Address) -> R Ok(()) } - diff --git a/src/lib.rs b/src/lib.rs index f7785fb9..b94d5b96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -647,4 +647,3 @@ impl ProofOfHeart { #[cfg(test)] mod tests; - diff --git a/src/tests/test_campaign_update.rs b/src/tests/test_campaign_update.rs index 15180645..99db9ee6 100644 --- a/src/tests/test_campaign_update.rs +++ b/src/tests/test_campaign_update.rs @@ -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); + 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.0, String::from_str(&env, "Original Title")); + assert_eq!(payload.1, String::from_str(&env, "Original Description")); + assert_eq!(payload.2, new_title); + assert_eq!(payload.3, new_desc); } #[test] @@ -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")); + let payload: (String, String, String, String) = soroban_sdk::FromVal::from_val(&env, &last_event.2); + assert_eq!(payload.0, String::from_str(&env, "Title V2")); + assert_eq!(payload.1, String::from_str(&env, "Description V2")); + assert_eq!(payload.2, String::from_str(&env, "Title V3")); + assert_eq!(payload.3, String::from_str(&env, "Description V3")); } #[test] From 1b054c4a5c9a28e258f8043e671d3bc08e14a203 Mon Sep 17 00:00:00 2001 From: bbkenny Date: Fri, 31 Jul 2026 23:58:10 +0100 Subject: [PATCH 4/4] style: apply cargo fmt to long line in test payload unpacking --- src/tests/test_campaign_update.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/tests/test_campaign_update.rs b/src/tests/test_campaign_update.rs index 99db9ee6..7a86115c 100644 --- a/src/tests/test_campaign_update.rs +++ b/src/tests/test_campaign_update.rs @@ -59,7 +59,8 @@ fn test_update_campaign_emits_title_and_description() { let events = env.events().all(); let last_event = events.last().unwrap(); - let payload: (String, String, String, String) = soroban_sdk::FromVal::from_val(&env, &last_event.2); + let payload: (String, String, String, String) = + soroban_sdk::FromVal::from_val(&env, &last_event.2); assert_eq!(payload.0, String::from_str(&env, "Original Title")); assert_eq!(payload.1, String::from_str(&env, "Original Description")); @@ -96,7 +97,8 @@ fn test_update_campaign_event_tracks_latest_description() { let events = env.events().all(); let last_event = events.last().unwrap(); - let payload: (String, String, String, String) = soroban_sdk::FromVal::from_val(&env, &last_event.2); + let payload: (String, String, String, String) = + soroban_sdk::FromVal::from_val(&env, &last_event.2); assert_eq!(payload.0, String::from_str(&env, "Title V2")); assert_eq!(payload.1, String::from_str(&env, "Description V2")); assert_eq!(payload.2, String::from_str(&env, "Title V3"));