Skip to content
Merged
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
8 changes: 8 additions & 0 deletions contracts/predict-iq/src/modules/bets.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ pub fn place_bet(
return Err(ErrorCode::MarketClosed);
}

// Hard-stop: once the resolution window begins, betting is locked regardless of market
// status. This closes the race window where an oracle result is known off-chain but
// `attempt_oracle_resolution` hasn't been called yet, preventing informed bettors from
// exploiting information asymmetry against uninformed participants.
if e.ledger().timestamp() >= market.resolution_deadline {
return Err(ErrorCode::ResolutionDeadlinePassed);
}

if outcome >= market.options.len() {
return Err(ErrorCode::InvalidOutcome);
}
Expand Down
42 changes: 42 additions & 0 deletions contracts/predict-iq/src/modules/bets_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,48 @@ fn test_place_bet_after_deadline() {
assert_eq!(result, Err(Ok(ErrorCode::MarketClosed)));
}

// fix/issue-60: Verify the hard stop on betting at the resolution deadline.
// Even if the market status is still Active (attempt_oracle_resolution hasn't been called yet),
// a bet placed at resolution_deadline + 1 must be rejected. This closes the race window where
// an oracle result is known off-chain but the on-chain status hasn't been updated, preventing
// informed bettors from exploiting information asymmetry.
#[test]
fn test_place_bet_rejected_one_second_after_resolution_deadline_while_still_active() {
let (env, client, _admin, user, token) = setup_test_with_token();

// Market created at t=500; deadline = 500+1000 = 1500, resolution_deadline = 500+2000 = 2500
env.ledger().with_mut(|li| li.timestamp = 500);
let market_id = create_simple_market(&client, &env, &user, &token);

// Confirm market is still Active (attempt_oracle_resolution has NOT been called)
let market = client.get_market(&market_id).unwrap();
assert_eq!(market.status, crate::types::MarketStatus::Active);

// Advance to exactly resolution_deadline + 1 (2501)
// The oracle result may already be known off-chain at this point
env.ledger().with_mut(|li| li.timestamp = 2501);

// Bet must be rejected with ResolutionDeadlinePassed, not succeed due to Active status
let result = client.try_place_bet(&user, &market_id, &0, &1000, &token, &None);
assert_eq!(result, Err(Ok(ErrorCode::ResolutionDeadlinePassed)));
}

#[test]
fn test_place_bet_allowed_one_second_before_resolution_deadline() {
let (env, client, _admin, user, token) = setup_test_with_token();

// Market created at t=500; deadline = 1500, resolution_deadline = 2500
env.ledger().with_mut(|li| li.timestamp = 500);
let market_id = create_simple_market(&client, &env, &user, &token);

// Advance to one second before resolution_deadline (2499) — still within betting window
env.ledger().with_mut(|li| li.timestamp = 2499);

// Bet should be accepted (not blocked by resolution deadline guard)
let result = client.try_place_bet(&user, &market_id, &0, &1000, &token, &None);
assert_ne!(result, Err(Ok(ErrorCode::ResolutionDeadlinePassed)));
}

#[test]
fn test_place_bet_on_resolved_market() {
let (env, client, _admin, user, token) = setup_test_with_token();
Expand Down
Loading