diff --git a/prediction_market/src/lib.rs b/prediction_market/src/lib.rs index 420d2cb..facdd13 100644 --- a/prediction_market/src/lib.rs +++ b/prediction_market/src/lib.rs @@ -642,7 +642,7 @@ impl PredictionMarketContract { pub fn cancel_refund(env: Env, user: Address, market_id: u64) -> Result { user.require_auth(); - let market = Self::load_market(&env, market_id)?; + let mut market = Self::load_market(&env, market_id)?; if !market.cancelled { return Err(MarketError::MarketNotCancelled); } @@ -660,8 +660,24 @@ impl PredictionMarketContract { } let gross = entry.gross; - entry.gross = 0; // idempotency guard + let net = entry.net; + // Issue #58: zero both gross (idempotency guard) and net so that + // get_bet no longer reports a staked amount after the refund. + entry.gross = 0; + entry.net = 0; env.storage().persistent().set(&bet_key, &entry); + + // Issue #58: decrement market totals so total_yes/total_no reflect + // that this bet has been refunded. BetEntry already carries is_yes + // and net, so no storage-model change is required. + let mkt_key = DataKey::Market(market_id); + if entry.is_yes { + market.total_yes = market.total_yes.saturating_sub(net); + } else { + market.total_no = market.total_no.saturating_sub(net); + } + env.storage().persistent().set(&mkt_key, &market); + // Read-time TTL refresh (issue #9): a refund must not be able to observe // an expired bet/market record — keep both alive so a user who returns // late to a cancelled market can still pull their refund. @@ -670,7 +686,7 @@ impl PredictionMarketContract { .extend_ttl(&bet_key, TTL_BUMP, TTL_HIGH); env.storage() .persistent() - .extend_ttl(&DataKey::Market(market_id), TTL_BUMP, TTL_HIGH); + .extend_ttl(&mkt_key, TTL_BUMP, TTL_HIGH); let cfg: Config = env.storage().instance().get(&DataKey::Cfg).unwrap(); token::Client::new(&env, &cfg.xlm_sac).transfer( diff --git a/prediction_market/src/tests.rs b/prediction_market/src/tests.rs index 062792f..fbfe95e 100644 --- a/prediction_market/src/tests.rs +++ b/prediction_market/src/tests.rs @@ -464,6 +464,52 @@ fn test_cancel_refund_double_claim_rejected() { t.client.cancel_refund(&user, &id); // should fail: NoBetFound (gross zeroed) } +// ── Issue #58: cancel_refund zeroes net and decrements market totals ───────── + +#[test] +fn test_cancel_refund_clears_net_and_market_totals() { + let t = setup(); + let id = create_test_market(&t); + let alice = Address::generate(&t.env); + let bob = Address::generate(&t.env); + fund_user(&t, &alice, 200_0000000); + fund_user(&t, &bob, 200_0000000); + + // Alice bets YES 100 XLM → net = 100 * 9800/10000 = 98 XLM + // Bob bets NO 50 XLM → net = 50 * 9800/10000 = 49 XLM + t.client.place_bet(&alice, &id, &true, &100_0000000_i128); + t.client.place_bet(&bob, &id, &false, &50_0000000_i128); + + let market_before = t.client.get_market(&id); + assert_eq!(market_before.total_yes, 98_0000000); + assert_eq!(market_before.total_no, 49_0000000); + + // Confirm get_bet reports staked net before cancel + let alice_bet_before = t.client.get_bet(&id, &alice); + assert_eq!(alice_bet_before.amount, 98_0000000); + + // Cancel the market then refund both bettors + t.client.cancel_market(&t.admin, &id); + t.client.cancel_refund(&alice, &id); + t.client.cancel_refund(&bob, &id); + + // Issue #58 fix: get_bet must return amount == 0 after refund + let alice_bet_after = t.client.get_bet(&id, &alice); + assert_eq!(alice_bet_after.amount, 0, "net should be zeroed after cancel_refund"); + + let bob_bet_after = t.client.get_bet(&id, &bob); + assert_eq!(bob_bet_after.amount, 0, "net should be zeroed after cancel_refund"); + + // Issue #58 fix: market totals must be decremented to 0 after all refunds + let market_after = t.client.get_market(&id); + assert_eq!(market_after.total_yes, 0, "total_yes should be 0 after alice's refund"); + assert_eq!(market_after.total_no, 0, "total_no should be 0 after bob's refund"); + + // Gross is still zeroed (idempotency guard unchanged) + assert_eq!(t.client.get_bet_gross(&id, &alice), 0); + assert_eq!(t.client.get_bet_gross(&id, &bob), 0); +} + // ── 19. cancel_refund on non-cancelled market rejected ──────────────────────── #[test]