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
6 changes: 4 additions & 2 deletions contract/contract/src/base/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,10 @@ pub enum CrowdfundingError {
PoolAlreadyClosed = 45,
PoolNotDisbursedOrRefunded = 46,
InvalidGoalUpdate = 47,
InsufficientFees = 48,
UserBlacklisted = 49,
/// Withdrawal exceeds the tracked platform-fee balance.
InsufficientPlatformFees = 48,
/// Withdrawal exceeds the tracked event-fee balance.
InsufficientEventFees = 49,
CampaignCancelled = 50,
}

Expand Down
73 changes: 71 additions & 2 deletions contract/contract/src/base/events.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,49 @@
#![allow(deprecated)]
use soroban_sdk::{Address, BytesN, Env, String, Symbol};
use soroban_sdk::{Address, BytesN, Env, String, Symbol, Vec};

use crate::base::types::PoolState;
use crate::base::types::{EventRecord, PoolState, StorageKey};

// ---------------------------------------------------------------------------
// Global event tracker
// ---------------------------------------------------------------------------

/// Increment the persistent event counter and append a record to `AllEvents`.
///
/// Uses persistent storage so the log survives ledger TTL expiry.
/// Called by every public event emitter in this module.
fn record_event(env: &Env, name: &str) {
let count_key = StorageKey::AllEventsCount;
let list_key = StorageKey::AllEvents;

// Increment counter (starts at 0 if not yet initialised)
let new_index: u64 = env
.storage()
.persistent()
.get::<_, u64>(&count_key)
.unwrap_or(0)
+ 1;

env.storage().persistent().set(&count_key, &new_index);

// Append a lightweight record to the global list
let mut list: Vec<EventRecord> = env
.storage()
.persistent()
.get::<_, Vec<EventRecord>>(&list_key)
.unwrap_or_else(|| Vec::new(env));

list.push_back(EventRecord {
index: new_index,
name: String::from_str(env, name),
timestamp: env.ledger().timestamp(),
});

env.storage().persistent().set(&list_key, &list);
}

// ---------------------------------------------------------------------------
// Event emitters
// ---------------------------------------------------------------------------

pub fn campaign_created(
env: &Env,
Expand All @@ -13,11 +55,13 @@ pub fn campaign_created(
) {
let topics = (Symbol::new(env, "campaign_created"), id, creator);
env.events().publish(topics, (title, goal, deadline));
record_event(env, "campaign_created");
}

pub fn campaign_goal_updated(env: &Env, id: BytesN<32>, new_goal: i128) {
let topics = (Symbol::new(env, "campaign_goal_updated"), id);
env.events().publish(topics, new_goal);
record_event(env, "campaign_goal_updated");
}

#[allow(clippy::too_many_arguments)]
Expand All @@ -29,6 +73,7 @@ pub fn pool_created(
) {
let topics = (Symbol::new(env, "pool_created"), pool_id, creator);
env.events().publish(topics, details);
record_event(env, "pool_created");
}

pub fn event_created(
Expand All @@ -42,46 +87,55 @@ pub fn event_created(
let topics = (Symbol::new(env, "event_created"), pool_id, creator);
env.events()
.publish(topics, (name, target_amount, deadline));
record_event(env, "event_created");
}

pub fn pool_state_updated(env: &Env, pool_id: u64, new_state: PoolState) {
let topics = (Symbol::new(env, "pool_state_updated"), pool_id);
env.events().publish(topics, new_state);
record_event(env, "pool_state_updated");
}

pub fn contract_paused(env: &Env, admin: Address, timestamp: u64) {
let topics = (Symbol::new(env, "contract_paused"), admin);
env.events().publish(topics, timestamp);
record_event(env, "contract_paused");
}

pub fn contract_unpaused(env: &Env, admin: Address, timestamp: u64) {
let topics = (Symbol::new(env, "contract_unpaused"), admin);
env.events().publish(topics, timestamp);
record_event(env, "contract_unpaused");
}

pub fn admin_renounced(env: &Env, admin: Address) {
let topics = (Symbol::new(env, "admin_renounced"), admin);
env.events().publish(topics, ());
record_event(env, "admin_renounced");
}

pub fn emergency_contact_updated(env: &Env, admin: Address, contact: Address) {
let topics = (Symbol::new(env, "emergency_contact_updated"), admin);
env.events().publish(topics, contact);
record_event(env, "emergency_contact_updated");
}

pub fn donation_made(env: &Env, campaign_id: BytesN<32>, contributor: Address, amount: i128) {
let topics = (Symbol::new(env, "donation_made"), campaign_id);
env.events().publish(topics, (contributor, amount));
record_event(env, "donation_made");
}

pub fn campaign_cancelled(env: &Env, id: BytesN<32>) {
let topics = (Symbol::new(env, "campaign_cancelled"), id);
env.events().publish(topics, ());
record_event(env, "campaign_cancelled");
}

pub fn campaign_refunded(env: &Env, id: BytesN<32>, contributor: Address, amount: i128) {
let topics = (Symbol::new(env, "campaign_refunded"), id, contributor);
env.events().publish(topics, amount);
record_event(env, "campaign_refunded");
}

pub fn contribution(
Expand All @@ -96,6 +150,7 @@ pub fn contribution(
let topics = (Symbol::new(env, "contribution"), pool_id, contributor);
env.events()
.publish(topics, (asset, amount, timestamp, is_private));
record_event(env, "contribution");
}

pub fn emergency_withdraw_requested(
Expand All @@ -107,26 +162,31 @@ pub fn emergency_withdraw_requested(
) {
let topics = (Symbol::new(env, "emergency_withdraw_requested"), admin);
env.events().publish(topics, (token, amount, unlock_time));
record_event(env, "emergency_withdraw_requested");
}

pub fn emergency_withdraw_executed(env: &Env, admin: Address, token: Address, amount: i128) {
let topics = (Symbol::new(env, "emergency_withdraw_executed"), admin);
env.events().publish(topics, (token, amount));
record_event(env, "emergency_withdraw_executed");
}

pub fn crowdfunding_token_set(env: &Env, admin: Address, token: Address) {
let topics = (Symbol::new(env, "crowdfunding_token_set"), admin);
env.events().publish(topics, token);
record_event(env, "crowdfunding_token_set");
}

pub fn creation_fee_set(env: &Env, admin: Address, fee: i128) {
let topics = (Symbol::new(env, "creation_fee_set"), admin);
env.events().publish(topics, fee);
record_event(env, "creation_fee_set");
}

pub fn creation_fee_paid(env: &Env, creator: Address, amount: i128) {
let topics = (Symbol::new(env, "creation_fee_paid"), creator);
env.events().publish(topics, amount);
record_event(env, "creation_fee_paid");
}

pub fn refund(
Expand All @@ -139,41 +199,49 @@ pub fn refund(
) {
let topics = (Symbol::new(env, "refund"), pool_id, contributor);
env.events().publish(topics, (asset, amount, timestamp));
record_event(env, "refund");
}

pub fn pool_closed(env: &Env, pool_id: u64, closed_by: Address, timestamp: u64) {
let topics = (Symbol::new(env, "pool_closed"), pool_id, closed_by);
env.events().publish(topics, timestamp);
record_event(env, "pool_closed");
}

pub fn platform_fees_withdrawn(env: &Env, to: Address, amount: i128) {
let topics = (Symbol::new(env, "platform_fees_withdrawn"), to);
env.events().publish(topics, amount);
record_event(env, "platform_fees_withdrawn");
}

pub fn event_fees_withdrawn(env: &Env, admin: Address, to: Address, amount: i128) {
let topics = (Symbol::new(env, "event_fees_withdrawn"), admin, to);
env.events().publish(topics, amount);
record_event(env, "event_fees_withdrawn");
}

pub fn address_blacklisted(env: &Env, admin: Address, address: Address) {
let topics = (Symbol::new(env, "address_blacklisted"), admin);
env.events().publish(topics, address);
record_event(env, "address_blacklisted");
}

pub fn address_unblacklisted(env: &Env, admin: Address, address: Address) {
let topics = (Symbol::new(env, "address_unblacklisted"), admin);
env.events().publish(topics, address);
record_event(env, "address_unblacklisted");
}

pub fn pool_metadata_updated(env: &Env, pool_id: u64, updater: Address, new_metadata_hash: String) {
let topics = (Symbol::new(env, "pool_metadata_updated"), pool_id, updater);
env.events().publish(topics, new_metadata_hash);
record_event(env, "pool_metadata_updated");
}

pub fn platform_fee_bps_set(env: &Env, admin: Address, fee_bps: u32) {
let topics = (Symbol::new(env, "platform_fee_bps_set"), admin);
env.events().publish(topics, fee_bps);
record_event(env, "platform_fee_bps_set");
}

pub fn ticket_sold(
Expand All @@ -187,4 +255,5 @@ pub fn ticket_sold(
let topics = (Symbol::new(env, "ticket_sold"), pool_id, buyer);
env.events()
.publish(topics, (price, event_amount, fee_amount));
record_event(env, "ticket_sold");
}
13 changes: 13 additions & 0 deletions contract/contract/src/base/types.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
use soroban_sdk::{contracttype, Address, BytesN, String, Vec};

/// A lightweight record stored for every emitted contract event.
/// Used to populate the global `AllEvents` list.
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct EventRecord {
/// Sequential index (1-based) assigned at emission time.
pub index: u64,
/// Short name matching the event's topic Symbol (e.g. "pool_created").
pub name: String,
/// Ledger timestamp at the moment the event was emitted.
pub timestamp: u64,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CampaignDetails {
Expand Down
53 changes: 44 additions & 9 deletions contract/contract/src/crowdfunding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ impl CrowdfundingTrait for CrowdfundingContract {
.instance()
.set(&event_pool_key, &(current_event + event_amount));

// Credit platform fee pool
// Credit platform fee pool (per-pool ledger for auditability)
let event_fee_key = StorageKey::EventPlatformFees(pool_id);
let current_fees: i128 = env.storage().instance().get(&event_fee_key).unwrap_or(0);
env.storage()
Expand Down Expand Up @@ -1732,14 +1732,16 @@ impl CrowdfundingTrait for CrowdfundingContract {
}

let platform_fees_key = StorageKey::PlatformFees;
let current_fees: i128 = env
let collected_fees: i128 = env
.storage()
.instance()
.get(&platform_fees_key)
.unwrap_or(0);

if amount > current_fees {
return Err(CrowdfundingError::InsufficientFees);
// Guard 1: amount must not exceed what the contract has tracked as
// collected platform fees — prevents draining pool-contribution funds.
if amount > collected_fees {
return Err(CrowdfundingError::InsufficientPlatformFees);
}

let token_key = StorageKey::CrowdfundingToken;
Expand All @@ -1751,11 +1753,20 @@ impl CrowdfundingTrait for CrowdfundingContract {

use soroban_sdk::token;
let token_client = token::Client::new(&env, &token_address);

// Guard 2: the on-chain token balance must cover the withdrawal.
// This catches any accounting drift between the fee counter and reality.
let contract_token_balance = token_client.balance(&env.current_contract_address());
if amount > contract_token_balance {
return Err(CrowdfundingError::InsufficientPlatformFees);
}

token_client.transfer(&env.current_contract_address(), &to, &amount);

// Deduct from the tracked fee balance — pool funds are never touched.
env.storage()
.instance()
.set(&platform_fees_key, &(current_fees - amount));
.set(&platform_fees_key, &(collected_fees - amount));

events::platform_fees_withdrawn(&env, to, amount);

Expand Down Expand Up @@ -1785,10 +1796,12 @@ impl CrowdfundingTrait for CrowdfundingContract {
}

let event_fees_key = StorageKey::EventFeeTreasury;
let current_fees: i128 = env.storage().instance().get(&event_fees_key).unwrap_or(0);
let collected_event_fees: i128 = env.storage().instance().get(&event_fees_key).unwrap_or(0);

if amount > current_fees {
return Err(CrowdfundingError::InsufficientFees);
// Guard 1: amount must not exceed the tracked event-fee treasury —
// prevents draining pool-contribution or platform-fee funds.
if amount > collected_event_fees {
return Err(CrowdfundingError::InsufficientEventFees);
}

let token_key = StorageKey::CrowdfundingToken;
Expand All @@ -1800,11 +1813,19 @@ impl CrowdfundingTrait for CrowdfundingContract {

use soroban_sdk::token;
let token_client = token::Client::new(&env, &token_address);

// Guard 2: on-chain balance must cover the withdrawal.
let contract_token_balance = token_client.balance(&env.current_contract_address());
if amount > contract_token_balance {
return Err(CrowdfundingError::InsufficientEventFees);
}

token_client.transfer(&env.current_contract_address(), &to, &amount);

// Deduct from the treasury — pool-contribution funds are never touched.
env.storage()
.instance()
.set(&event_fees_key, &(current_fees - amount));
.set(&event_fees_key, &(collected_event_fees - amount));

events::event_fees_withdrawn(&env, admin, to, amount);

Expand Down Expand Up @@ -1901,6 +1922,20 @@ impl CrowdfundingTrait for CrowdfundingContract {
env.deployer().update_current_contract_wasm(new_wasm_hash);
Ok(())
}

fn get_all_events_count(env: Env) -> u64 {
env.storage()
.persistent()
.get::<_, u64>(&StorageKey::AllEventsCount)
.unwrap_or(0)
}

fn get_all_events(env: Env) -> Vec<crate::base::types::EventRecord> {
env.storage()
.persistent()
.get::<_, Vec<crate::base::types::EventRecord>>(&StorageKey::AllEvents)
.unwrap_or_else(|| Vec::new(&env))
}
}

impl CrowdfundingContract {
Expand Down
6 changes: 6 additions & 0 deletions contract/contract/src/interfaces/crowdfunding.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,4 +217,10 @@ pub trait CrowdfundingTrait {
) -> Result<(i128, i128), CrowdfundingError>;

fn upgrade_contract(env: Env, new_wasm_hash: BytesN<32>) -> Result<(), CrowdfundingError>;

/// Returns the total number of events ever emitted by this contract.
fn get_all_events_count(env: Env) -> u64;

/// Returns the full list of emitted event records (index, name, timestamp).
fn get_all_events(env: Env) -> Vec<crate::base::types::EventRecord>;
}
Loading
Loading