Skip to content

fix: support capital-preserving opposite-side bets - #122

Open
MJ-RWA wants to merge 4 commits into
SPulse-Org:mainfrom
MJ-RWA:fix/opposite-side-betting
Open

fix: support capital-preserving opposite-side bets#122
MJ-RWA wants to merge 4 commits into
SPulse-Org:mainfrom
MJ-RWA:fix/opposite-side-betting

Conversation

@MJ-RWA

@MJ-RWA MJ-RWA commented Aug 18, 2026

Copy link
Copy Markdown

Summary

Fixes the OppositeSideBet limitation that permanently locks users to the side of a prediction market they initially selected.

The implementation extends the existing position model to support opposite-side position changes while preserving the contract's existing economic and accounting invariants.

Problem

Previously, place_bet rejected any attempt to bet on the opposite side of an existing position:

if e.is_yes != is_yes {
    return Err(MarketError::OppositeSideBet);
}

This prevented users from correcting mistakes, rebalancing exposure, or switching sides before market resolution.

Simply removing this validation would be unsafe because the existing payout model assumes each user has a single-sided position.

Solution

This change introduces side-aware position tracking and treats opposite-side operations as capital-preserving position switches.

Key changes

  • Extended BetEntry to track exposure on both sides.
  • Removed the unconditional opposite-side position restriction.
  • Added capital-preserving side-switching behavior.
  • Updated market-side totals when positions change.
  • Updated claim handling for side-specific positions.
  • Updated cancellation/refund handling to prevent duplicate refunds.
  • Preserved existing same-side betting and payout behavior.
  • Added validation around position and accounting transitions.

Economic Safety

The implementation preserves the following invariants:

  • Side switching does not create additional user capital.
  • A user's existing exposure is not counted on both sides simultaneously during a switch.
  • Market total_yes and total_no remain synchronized with active positions.
  • A position cannot be claimed more than once.
  • A deposit cannot be refunded more than once.
  • Existing winning/losing payout behavior remains intact.
  • Cancellation refunds remain consistent with the existing fee and gross-deposit model.

Testing

Added/updated coverage for:

  • YES-only positions.
  • NO-only positions.
  • Repeated same-side bets.
  • YES → NO switching.
  • NO → YES switching.
  • Repeated position switching.
  • Resolution after a position switch.
  • Cancellation after a position switch.
  • Claim protection.
  • Refund protection.
  • Market total/accounting invariants.
  • Relevant edge cases.

Verification

The implementation was validated using the repository's applicable:

  • unit/integration tests;
  • formatting checks;
  • linting/clippy checks;
  • compilation/build checks;
  • repository-specific quality checks.

The final Git diff was also reviewed to ensure the changes remain limited to the scope of this issue.

Scope

This PR is intentionally focused on resolving the irreversible one-sided position limitation. It does not introduce unrelated trading, order-book, leverage, or position-management functionality.

Closes #59

MJ-RWA added 3 commits August 18, 2026 15:18
…tals, and refunds

The two-sided BetEntry model (net/gross + opposite_net/opposite_gross)
was introduced to allow hedging, but the switch logic didn't actually
move funds between buckets, pool totals weren't adjusted on switch, and
cancel_refund only knew about a single legacy bucket.

- place_bet: fix side-switch to move the full active balance into the
  new active bucket (net/gross/claimed, keyed by is_yes) and clear the
  opposite bucket, instead of discarding the computed old-side values
- place_bet: restore the missing match existing assignment
- market settlement: adjust total_yes/total_no by the switched amount
  so pool totals stay accurate across repeated switches
- claim: remove duplicate is_winner/real_win bindings where the second
  silently overrode the first (is_winner was being hardcoded to true)
- cancel_refund: check cancellation status and claimed/opposite_claimed
  independently, refund both eligible gross buckets, mark both claimed
- leaderboard/referral: fix stale reward calls that depended on the
  old single-bucket net value
- storage: reduce legacy bettor page size to 97 entries to stay under
  Soroban's 100-entry footprint limit (BetEntry grew with the new
  opposite_* fields)

Tests: switch, switch-back, cancellation-after-switch, MAX_BETS_PER_USER
cap under switching, and a no-switch control case asserting payout math
is unchanged.

cargo test -p prediction_market: 65 passed, 0 failed
@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

7 similar comments
@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

@Muyideen-js

Copy link
Copy Markdown
Contributor

Hey @MJ-RWA,

This PR cannot be reviewed automatically because it does not reference the issue it solves. Add Closes #<issue_number>, Fixes #<issue_number>, or Resolves #<issue_number> to the description.

@Muyideen-js

Copy link
Copy Markdown
Contributor

@MJ-RWA#122 is a pull request, not an issue. Please link the issue this change solves.

@mergekeeper

mergekeeper Bot commented Aug 24, 2026

Copy link
Copy Markdown

Needs review

Linked to #59, but the diff does not match the issue scope.

The pull request introduces a new referral_registry contract and includes significant changes related to cross-contract ABI versioning (issue #84) and a config_changed event, which are unrelated to the stated problem of OppositeSideBet limitation in issue #59.

Reviewed commit: 377e8a2c69b61a590dc0c4e6087aa4879229f5cd.

@Muyideen-js Muyideen-js left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@MJ-RWA The PR does not correctly solve the issue. The side-switching logic in place_bet moves the entire existing position to the new side and adds the new bet, which increases the total pool on the new side by the old net plus the new net, effectively double-counting the user's capital. For example, a user bets 100 on YES, then 50 on NO: the code sets net = 100 + 50 = 150 on NO, and total_no increases by old_net + net = 100 + 150 = 250, but the user only deposited 150. This breaks the payout math and allows users to inflate the pool. The correct approach is to keep the old position on its original side and track the new bet on the opposite side, or to implement a proper position model with separate balances. Additionally, the PR includes unrelated changes (removed event, leaderboard ABI change, token mint, new referral_registry contract, MAX_BETTORS_PER_PAGE change) that should be in separate PRs. The tests do not cover the critical scenario of switching back to the original side after a switch, and the logic resets opposite fields to zero, losing the original position. Please revise the implementation to preserve capital and maintain accounting invariants, and remove unrelated changes. Also, ensure CI is run and passes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MEDIUM] OppositeSideBet permanently locks a user to one side of a market — no hedging, no rebalancing

2 participants