From 1cf3c7e453439274e4e8efde647cbb8b3c7f7887 Mon Sep 17 00:00:00 2001 From: cybermax4200 Date: Sat, 20 Jun 2026 13:34:47 +0100 Subject: [PATCH 1/2] fix(tools): reject non-positive and overflow amounts in CampaignTotals::increment --- crates/tools/src/campaign_totals.rs | 55 +++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/crates/tools/src/campaign_totals.rs b/crates/tools/src/campaign_totals.rs index 5e7f95e..25800b6 100644 --- a/crates/tools/src/campaign_totals.rs +++ b/crates/tools/src/campaign_totals.rs @@ -1,3 +1,4 @@ +use anyhow::{anyhow, Result}; use std::collections::HashMap; /// In-memory store for per-campaign donation totals, grouped by asset. @@ -16,12 +17,16 @@ impl CampaignTotals { } /// Adds `amount` to the running total for `campaign_id` + `asset` and returns the new total. - #[must_use] + /// + /// Returns `Err` if `amount` is non-positive or if the addition would overflow. #[inline] - pub fn increment(&mut self, campaign_id: u64, asset: &str, amount: i128) -> i128 { + pub fn increment(&mut self, campaign_id: u64, asset: &str, amount: i128) -> Result { + if amount <= 0 { + return Err(anyhow!("CampaignTotals::increment requires positive amount; got {}", amount)); + } let entry = self.asset_totals.entry((campaign_id, asset.to_string())).or_insert(0); - *entry += amount; - *entry + *entry = entry.checked_add(amount).ok_or_else(|| anyhow!("overflow in CampaignTotals"))?; + Ok(*entry) } /// Returns the total for a specific `campaign_id` + `asset`, or 0 if none recorded. @@ -64,16 +69,16 @@ mod tests { #[test] fn increments_per_asset() { let mut totals = CampaignTotals::new(); - totals.increment(1, "XLM", 500); - totals.increment(1, "XLM", 300); + totals.increment(1, "XLM", 500).unwrap(); + totals.increment(1, "XLM", 300).unwrap(); assert_eq!(totals.get(1, "XLM"), 800); } #[test] fn different_assets_are_independent() { let mut totals = CampaignTotals::new(); - totals.increment(1, "XLM", 100); - totals.increment(1, "USDC", 200); + totals.increment(1, "XLM", 100).unwrap(); + totals.increment(1, "USDC", 200).unwrap(); assert_eq!(totals.get(1, "XLM"), 100); assert_eq!(totals.get(1, "USDC"), 200); } @@ -81,8 +86,8 @@ mod tests { #[test] fn different_campaigns_are_independent() { let mut totals = CampaignTotals::new(); - totals.increment(1, "XLM", 100); - totals.increment(2, "XLM", 200); + totals.increment(1, "XLM", 100).unwrap(); + totals.increment(2, "XLM", 200).unwrap(); assert_eq!(totals.get(1, "XLM"), 100); assert_eq!(totals.get(2, "XLM"), 200); } @@ -90,8 +95,8 @@ mod tests { #[test] fn get_all_assets_returns_correct_map() { let mut totals = CampaignTotals::new(); - totals.increment(1, "XLM", 500); - totals.increment(1, "USDC", 300); + totals.increment(1, "XLM", 500).unwrap(); + totals.increment(1, "USDC", 300).unwrap(); let map = totals.get_all_assets(1); assert_eq!(map.get("XLM"), Some(&500)); assert_eq!(map.get("USDC"), Some(&300)); @@ -100,8 +105,30 @@ mod tests { #[test] fn campaign_total_aggregates_all_assets() { let mut totals = CampaignTotals::new(); - totals.increment(1, "XLM", 500); - totals.increment(1, "USDC", 300); + totals.increment(1, "XLM", 500).unwrap(); + totals.increment(1, "USDC", 300).unwrap(); assert_eq!(totals.get_campaign_total(1), 800); } + + #[test] + fn rejects_negative_amount() { + let mut totals = CampaignTotals::new(); + let err = totals.increment(1, "XLM", -100).unwrap_err(); + assert!(err.to_string().contains("positive amount")); + } + + #[test] + fn rejects_zero_amount() { + let mut totals = CampaignTotals::new(); + let err = totals.increment(1, "XLM", 0).unwrap_err(); + assert!(err.to_string().contains("positive amount")); + } + + #[test] + fn rejects_overflow() { + let mut totals = CampaignTotals::new(); + totals.increment(1, "XLM", i128::MAX).unwrap(); + let err = totals.increment(1, "XLM", 1).unwrap_err(); + assert!(err.to_string().contains("overflow")); + } } From 3e5a1a282d0b316baefa1618c657dbdab4debba3 Mon Sep 17 00:00:00 2001 From: cybermax4200 Date: Mon, 22 Jun 2026 21:00:15 +0100 Subject: [PATCH 2/2] ci: add GitHub Actions workflow to run contract tests --- .github/workflows/ci.yml | 52 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a0b994b --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +on: + push: + branches: ["main", "fix/**", "feat/**", "chore/**"] + pull_request: + branches: ["main"] + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Build & Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + targets: wasm32-unknown-unknown,wasm32v1-none + components: rustfmt,clippy,rust-src + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + restore-keys: ${{ runner.os }}-cargo- + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy --workspace -- -D warnings + + - name: Build (native) + run: cargo build --workspace + + - name: Build campaign WASM + run: cargo build -p orbitchain-campaign --target wasm32-unknown-unknown --release + + - name: Run contract tests + run: | + cargo test -p orbitchain-campaign + cargo test -p orbitchain-core + cargo test -p orbitchain-common + cargo test -p orbitchain-token-bridge