Skip to content
Closed
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
74 changes: 49 additions & 25 deletions .github/workflows/contracts-ci.yml
Original file line number Diff line number Diff line change
@@ -1,49 +1,73 @@
name: Contract CI/CD Pipeline

on:
push:
branches: [main, develop]
paths:
- 'contract/**'

pull_request:
branches: [ main, develop ]
paths:
- 'contract/**'
push:
branches: [main, develop]
paths:
- "contract/**"
pull_request:
branches: [main, develop]
paths:
- "contract/**"

jobs:
build:
build-and-test:
name: Contract Build & Test
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Rust
uses: actions-rs/toolchain@v1
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
profile: minimal
override: true
components: rustfmt, clippy
targets: wasm32-unknown-unknown

- name: Cache Cargo dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
contract/target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-cargo-

- name: Install Stellar CLI
run: |
curl -s https://get.stellar.org | bash
echo "$HOME/.stellar/bin" >> $GITHUB_PATH

- name: Check formatting
- name: Check Formatting
run: cargo fmt --all -- --check
working-directory: ./contract/contract
working-directory: ./contract

- name: Run contract tests
- name: Lint with Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
working-directory: ./contract

- name: Run Contract Tests
run: cargo test
working-directory: ./contract/contract
working-directory: ./contract

- name: Build Contracts (WASM)
run: cargo build --target wasm32-unknown-unknown --release
working-directory: ./contract

- name: Build contracts
run: cargo build --release
working-directory: ./contract/contract
- name: Verify WASM Build
run: |
ls -l target/wasm32-unknown-unknown/release/*.wasm
working-directory: ./contract

- name: Deploy to Stellar testnet
- name: Deploy to Stellar Testnet (Manual/Workflow Dispatch)
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop')
run: echo "Add Stellar deployment script here"
working-directory: ./contract/contract
working-directory: ./contract
env:
STELLAR_SECRET: ${{ secrets.STELLAR_SECRET }}
1 change: 1 addition & 0 deletions contract/contract/src/base/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ pub enum CrowdfundingError {
PoolAlreadyClosed = 45,
PoolNotDisbursedOrRefunded = 46,
InsufficientFees = 47,
FlashDonationDetected = 54,
}
2 changes: 2 additions & 0 deletions contract/contract/src/base/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub struct Contribution {
pub campaign_id: BytesN<32>,
pub contributor: Address,
pub amount: i128,
pub last_donation_ledger: u32,
}

#[contracttype]
Expand Down Expand Up @@ -164,6 +165,7 @@ pub struct PoolContribution {
pub contributor: Address,
pub amount: i128,
pub asset: Address,
pub last_donation_ledger: u32,
}

#[contracttype]
Expand Down
15 changes: 15 additions & 0 deletions contract/contract/src/crowdfunding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,12 +327,14 @@ impl CrowdfundingTrait for CrowdfundingContract {
campaign_id: campaign_id.clone(),
contributor: donor.clone(),
amount: 0,
last_donation_ledger: 0,
});

let updated_contribution = Contribution {
campaign_id: campaign_id.clone(),
contributor: donor.clone(),
amount: existing_contribution.amount + amount,
last_donation_ledger: env.ledger().sequence(),
};
env.storage()
.instance()
Expand Down Expand Up @@ -720,6 +722,7 @@ impl CrowdfundingTrait for CrowdfundingContract {
contributor: contributor.clone(),
amount: 0,
asset: asset.clone(),
last_donation_ledger: 0,
});

// Only increment contributor_count if this is a new contributor
Expand All @@ -738,6 +741,7 @@ impl CrowdfundingTrait for CrowdfundingContract {
contributor: contributor.clone(),
amount: existing_contribution.amount + amount,
asset: asset.clone(),
last_donation_ledger: env.ledger().sequence(),
};
env.storage()
.instance()
Expand Down Expand Up @@ -818,6 +822,9 @@ impl CrowdfundingTrait for CrowdfundingContract {
return Err(CrowdfundingError::NoContributionToRefund);
}

// Flash donation protection
verify_ledger_age(&env, contribution.last_donation_ledger)?;

// Transfer tokens back to contributor
use soroban_sdk::token;
let token_client = token::Client::new(&env, &contribution.asset);
Expand Down Expand Up @@ -848,6 +855,7 @@ impl CrowdfundingTrait for CrowdfundingContract {
contributor: contributor.clone(),
amount: 0,
asset: contribution.asset.clone(),
last_donation_ledger: contribution.last_donation_ledger,
};
env.storage()
.instance()
Expand Down Expand Up @@ -1090,3 +1098,10 @@ impl CrowdfundingTrait for CrowdfundingContract {
Ok(())
}
}

fn verify_ledger_age(env: &Env, last_ledger: u32) -> Result<(), CrowdfundingError> {
if last_ledger == env.ledger().sequence() {
return Err(CrowdfundingError::FlashDonationDetected);
}
Ok(())
}
80 changes: 80 additions & 0 deletions contract/contract/test/flash_donation_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
#![cfg(test)]

use soroban_sdk::{
testutils::{Address as _, Ledger},
Address, Env, String,
};

use crate::{
base::{
errors::CrowdfundingError,
types::{PoolConfig},
},
crowdfunding::{CrowdfundingContract, CrowdfundingContractClient},
};

fn setup_test(env: &Env) -> (CrowdfundingContractClient<'_>, Address, Address) {
env.mock_all_auths();
let contract_id = env.register(CrowdfundingContract, ());
let client = CrowdfundingContractClient::new(env, &contract_id);

let admin = Address::generate(env);
let token_admin = Address::generate(env);
let token_contract = env.register_stellar_asset_contract_v2(token_admin.clone());
let token_address = token_contract.address();

client.initialize(&admin, &token_address, &0);

(client, admin, token_address)
}

#[test]
fn test_flash_donation_protection() {
let env = Env::default();
let (client, _, token_address) = setup_test(&env);

let creator = Address::generate(&env);
let donor = Address::generate(&env);

// Set initial sequence
env.ledger().with_mut(|li| li.sequence = 100);

// 1. Create a pool
let name = String::from_str(&env, "Flash Test");
let target = 10_000i128;
let duration = 3600; // 1 hour
let pool_config = PoolConfig {
name,
description: String::from_str(&env, "Desc"),
target_amount: target,
is_private: false,
duration,
created_at: env.ledger().timestamp(),
};
let pool_id = client.create_pool(&creator, &pool_config);

// Give tokens to donor
let token_admin = Address::generate(&env); // Not used but we need some admin logic if mocked
let token = soroban_sdk::token::StellarAssetClient::new(&env, &token_address);
token.mint(&donor, &5000);

// 2. Donate
client.contribute(&pool_id, &donor, &token_address, &1000, &false);

// 3. Attempt refund in the SAME ledger sequence
// First, expire the pool to allow refund
env.ledger().with_mut(|li| {
li.timestamp += duration + 700000; // Pass deadline + 7 days grace period
});

let result = client.try_refund(&pool_id, &donor);
assert_eq!(result, Err(Ok(CrowdfundingError::FlashDonationDetected)));

// 4. Advance ledger sequence and try again
env.ledger().with_mut(|li| {
li.sequence += 1;
});

let result_success = client.try_refund(&pool_id, &donor);
assert!(result_success.is_ok());
}
1 change: 1 addition & 0 deletions contract/contract/test/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
mod close_pool_test;
mod create_pool;
mod crowdfunding_test;
mod flash_donation_test;
mod verify_cause;
13 changes: 6 additions & 7 deletions frontend/src/app/contact-us/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import { useState } from "react";
import React, { useState } from "react";
import { ArrowUpRight, ChevronDown } from "lucide-react";
import Navigation from "../../components/Navigation";
import Footer from "../../components/Footer";
Expand Down Expand Up @@ -27,7 +27,7 @@ export default function ContactPage() {
setError("");

// Simulate successful submission
console.log({ fullName, subject, message, email });
// console.log({ fullName, subject, message, email });

setSuccess(true);

Expand Down Expand Up @@ -146,11 +146,10 @@ export default function ContactPage() {
type="submit"
disabled={!isFormValid}
className={`flex items-center justify-center gap-2 px-8 py-3 rounded-t-lg rounded-b-[18px] font-semibold transition-all duration-300
${
isFormValid
? "bg-[#50C878] text-[#0F172A] cursor-pointer"
: "bg-[#50C878]/60 text-[#0F172A]/70 cursor-not-allowed"
}`}
${isFormValid
? "bg-[#50C878] text-[#0F172A] cursor-pointer"
: "bg-[#50C878]/60 text-[#0F172A]/70 cursor-not-allowed"
}`}
>
SEND MESSAGE
<ArrowUpRight size={16} aria-hidden={true} />
Expand Down
28 changes: 28 additions & 0 deletions frontend/src/app/dashboard/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import DashboardHeader from "@/components/DashboardHeader";
import { PoolGrid } from "@/components/PoolGrid";

export default function DashboardPage() {
return (
<div className="min-h-screen bg-[#0F172A]">
<DashboardHeader />

<main className="mx-auto max-w-7xl px-4 pb-20 pt-10 sm:px-6 lg:px-8">
{/* Page heading */}
<section className="mb-10">
<h1 className="text-3xl font-extrabold text-white">Dashboard</h1>
<p className="mt-1 text-sm text-slate-400">
Manage your donation pools and track contributions.
</p>
</section>

{/* Pools */}
<section>
<h2 className="mb-6 text-lg font-semibold text-slate-200">
Active Pools
</h2>
<PoolGrid />
</section>
</main>
</div>
);
}
1 change: 1 addition & 0 deletions frontend/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react";
import type { Metadata } from "next";
// import { DM_Sans, Geist, Geist_Mono } from "next/font/google";
// import "./globals.css";
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/app/stellar-wallets-kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ export async function connect(callback?: () => Promise<void>) {
await setWallet(option.id);
if (callback) await callback();
} catch (e) {
console.error(e);
// console.error(e);
}
return option.id;
},
Expand Down
2 changes: 2 additions & 0 deletions frontend/src/app/terms-&-conditions/Page.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import React from "react";

export default function TermsPage() {
return (
<div>
Expand Down
Loading