Skip to content

Commit 489a8ab

Browse files
Merge pull request #494 from DevAyomi/feature/event-emission-audit
Feature/event emission audit
2 parents f5a6f90 + 8e81907 commit 489a8ab

File tree

9 files changed

+950
-323
lines changed

9 files changed

+950
-323
lines changed

contracts/predictify-hybrid/src/bets.rs

Lines changed: 144 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ use soroban_sdk::{contracttype, symbol_short, Address, Env, Map, String, Symbol,
2424
use crate::errors::Error;
2525
use crate::events::EventEmitter;
2626
use crate::markets::{MarketStateManager, MarketUtils, MarketValidator};
27-
use crate::types::{Bet, BetLimits, BetStatus, BetStats, Market, MarketState};
27+
use crate::types::{Bet, BetLimits, BetStats, BetStatus, Market, MarketState};
2828
// use crate::validation;
2929

3030
// ===== CONSTANTS =====
@@ -517,6 +517,8 @@ impl BetManager {
517517
for i in 0..bet_count {
518518
if let Some(bet_key) = bets.get(i) {
519519
if let Some(mut bet) = BetStorage::get_bet(env, market_id, &bet_key) {
520+
let old_status = bet.status;
521+
520522
// Determine if bet won or lost (check if outcome is in winning outcomes)
521523
if winning_outcomes.contains(&bet.outcome) {
522524
bet.mark_as_won();
@@ -527,8 +529,36 @@ impl BetManager {
527529
// Update bet status
528530
BetStorage::store_bet(env, &bet)?;
529531

530-
// Skip event emission to avoid potential segfaults
531-
// Events can be emitted separately if needed
532+
if old_status != bet.status {
533+
let old_status_str = match old_status {
534+
crate::types::BetStatus::Active => String::from_str(env, "Active"),
535+
crate::types::BetStatus::Won => String::from_str(env, "Won"),
536+
crate::types::BetStatus::Lost => String::from_str(env, "Lost"),
537+
crate::types::BetStatus::Refunded => String::from_str(env, "Refunded"),
538+
crate::types::BetStatus::Cancelled => {
539+
String::from_str(env, "Cancelled")
540+
}
541+
};
542+
let new_status_str = match bet.status {
543+
crate::types::BetStatus::Active => String::from_str(env, "Active"),
544+
crate::types::BetStatus::Won => String::from_str(env, "Won"),
545+
crate::types::BetStatus::Lost => String::from_str(env, "Lost"),
546+
crate::types::BetStatus::Refunded => String::from_str(env, "Refunded"),
547+
crate::types::BetStatus::Cancelled => {
548+
String::from_str(env, "Cancelled")
549+
}
550+
};
551+
552+
// Resolution status is visible on-ledger for indexers and auditors.
553+
EventEmitter::emit_bet_status_updated(
554+
env,
555+
market_id,
556+
&bet.user,
557+
&old_status_str,
558+
&new_status_str,
559+
None,
560+
);
561+
}
532562
}
533563
}
534564
}
@@ -640,6 +670,116 @@ impl BetManager {
640670

641671
Ok(payout)
642672
}
673+
/// Cancel a bet before the market deadline and refund the user.
674+
///
675+
/// This function allows users to cancel their active bets before the market
676+
/// deadline, receiving a full refund of their locked funds.
677+
///
678+
/// # Parameters
679+
///
680+
/// - `env` - The Soroban environment
681+
/// - `user` - Address of the user cancelling the bet
682+
/// - `market_id` - Symbol identifying the market
683+
///
684+
/// # Returns
685+
///
686+
/// Returns `Ok(())` on successful cancellation and refund,
687+
/// or `Err(Error)` if cancellation fails.
688+
///
689+
/// # Errors
690+
///
691+
/// - `Error::NothingToClaim` - User has no bet on this market
692+
/// - `Error::MarketNotFound` - Market does not exist
693+
/// - `Error::MarketClosed` - Market deadline has passed
694+
/// - `Error::InvalidState` - Bet is not in Active status
695+
///
696+
/// # Security
697+
///
698+
/// - Requires user authentication via `require_auth()`
699+
/// - Only the bettor can cancel their own bet
700+
/// - Can only cancel before market deadline
701+
/// - Funds are refunded atomically with status update
702+
///
703+
/// # Example
704+
///
705+
/// ```rust
706+
/// BetManager::cancel_bet(
707+
/// &env,
708+
/// user.clone(),
709+
/// Symbol::new(&env, "BTC_100K"),
710+
/// )?;
711+
/// ```
712+
pub fn cancel_bet(env: &Env, user: Address, market_id: Symbol) -> Result<(), Error> {
713+
// Require authentication from the user
714+
user.require_auth();
715+
716+
// Get user's bet
717+
let mut bet = BetStorage::get_bet(env, &market_id, &user).ok_or(Error::NothingToClaim)?;
718+
719+
// Ensure bet is active
720+
if !bet.is_active() {
721+
return Err(Error::InvalidState);
722+
}
723+
724+
// Get market and validate it hasn't ended
725+
let market = MarketStateManager::get_market(env, &market_id)?;
726+
let current_time = env.ledger().timestamp();
727+
728+
if current_time >= market.end_time {
729+
return Err(Error::MarketClosed);
730+
}
731+
732+
// Refund the locked funds
733+
BetUtils::unlock_funds(env, &user, bet.amount)?;
734+
735+
// Mark bet as cancelled
736+
bet.status = BetStatus::Cancelled;
737+
BetStorage::store_bet(env, &bet)?;
738+
739+
// Update market betting stats
740+
Self::update_market_bet_stats_on_cancel(env, &market_id, &bet.outcome, bet.amount)?;
741+
742+
// Emit bet cancelled event
743+
EventEmitter::emit_bet_status_updated(
744+
env,
745+
&market_id,
746+
&user,
747+
&String::from_str(env, "Active"),
748+
&String::from_str(env, "Cancelled"),
749+
Some(bet.amount),
750+
);
751+
752+
Ok(())
753+
}
754+
755+
/// Update market betting statistics after a bet cancellation.
756+
fn update_market_bet_stats_on_cancel(
757+
env: &Env,
758+
market_id: &Symbol,
759+
outcome: &String,
760+
amount: i128,
761+
) -> Result<(), Error> {
762+
let mut stats = BetStorage::get_market_bet_stats(env, market_id);
763+
764+
// Update totals
765+
stats.total_bets = stats.total_bets.saturating_sub(1);
766+
stats.total_amount_locked = stats.total_amount_locked.saturating_sub(amount);
767+
stats.unique_bettors = stats.unique_bettors.saturating_sub(1);
768+
769+
// Update outcome totals
770+
let current_outcome_total = stats.outcome_totals.get(outcome.clone()).unwrap_or(0);
771+
let new_total = current_outcome_total.saturating_sub(amount);
772+
if new_total > 0 {
773+
stats.outcome_totals.set(outcome.clone(), new_total);
774+
} else {
775+
stats.outcome_totals.remove(outcome.clone());
776+
}
777+
778+
// Store updated stats
779+
BetStorage::store_market_bet_stats(env, market_id, &stats)?;
780+
781+
Ok(())
782+
}
643783
}
644784

645785
// ===== BET STORAGE =====
@@ -829,7 +969,7 @@ impl BetValidator {
829969
let limits = get_effective_bet_limits(env, market_id);
830970
// Temporarily disabled due to validation module being disabled
831971
// validation::validate_bet_amount_against_limits(amount, &limits)
832-
972+
833973
// Simple validation for now
834974
if amount < limits.min_bet || amount > limits.max_bet {
835975
return Err(Error::InvalidInput);

0 commit comments

Comments
 (0)