> ## Documentation Index
> Fetch the complete documentation index at: https://docs.mouth.app/llms.txt
> Use this file to discover all available pages before exploring further.

# MouthBet

> The individual bet escrow contract deployed for each PvP and Open PvP bet.

## Overview

Each `MouthBet` contract is an escrow that holds USDC deposits from the Challenger and one or more Opponents, then distributes funds based on the resolution outcome.

Uses `initialize()` instead of a constructor, making it **clone-ready** for a future migration to EIP-1167 Minimal Proxy. Inherits from `Initializable` and `ReentrancyGuardUpgradeable` (OpenZeppelin v5). The contract itself is **NOT upgradeable** — once deployed, the rules of a bet are immutable.

The contract supports two modes:

* **PvP (1v1)**: A specific opponent is set at creation. Only that address can accept.
* **Open PvP**: No specific opponent (`address(0)`). Anyone can deposit, and multiple opponents can each fill a portion of the total.

## Initialization

Called once by the Factory immediately after deployment. Replaces the constructor.

```solidity theme={null}
function initialize(
    address _factory,
    address _challenger,
    address _opponent,
    uint256 _challengerAmount,
    uint256 _totalOpponentAmount,
    uint256 _odds,
    uint256 _expirationDate,
    uint256 _acceptanceDeadline,
    string calldata _title,
    bool _disputeable
) external initializer
```

## State Machine

### PvP (1v1) — Disputeable

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending: Contract deployed
    Pending --> Active: Opponent deposits
    Pending --> Cancelled: Challenger withdraws
    Pending --> Expired: Deadline passes (challenger or resolver withdraws)
    Active --> Resolved: Resolver submits outcome (48h dispute window starts)
    Resolved --> Finalized: Dispute window expires / dispute resolved
    Finalized --> Claimed: Winner claims funds
    Active --> Cancelled: Mutual cancellation
    Cancelled --> [*]: Funds returned
    Claimed --> [*]: Done
```

### PvP (1v1) — Non-Disputeable

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending: Contract deployed
    Pending --> Active: Opponent deposits
    Pending --> Cancelled: Challenger withdraws
    Pending --> Expired: Deadline passes (challenger or resolver withdraws)
    Active --> Finalized: Resolver submits outcome (immediately finalized)
    Finalized --> Claimed: Winner claims / resolver distributes
    Active --> Cancelled: Mutual cancellation
    Cancelled --> [*]: Funds returned
    Claimed --> [*]: Done
```

### Open PvP (Partially Fillable)

```mermaid theme={null}
stateDiagram-v2
    [*] --> Pending: Contract deployed
    Pending --> Filling: First opponent deposits
    Filling --> Filling: More opponents deposit
    Filling --> Active: Fully filled OR acceptance deadline passes (partial fill)
    Pending --> Cancelled: Challenger withdraws (no fills)
    Pending --> Expired: Deadline passes, no fills (challenger or resolver withdraws)
    Active --> Resolved: Resolver submits outcome (disputeable: 48h window)
    Active --> Finalized: Resolver submits outcome (non-disputeable: immediate)
    Resolved --> Finalized: Dispute window expires / dispute resolved
    Finalized --> Claimed: Winner(s) claim / resolver distributes
    Active --> Cancelled: Mutual cancellation
    Cancelled --> [*]: Funds returned
    Claimed --> [*]: Done
```

<Note>
  When `disputeable = true`, the `Resolved` status encompasses the 48-hour dispute window. Once the window passes (or a dispute is resolved), the bet transitions to `Finalized`. When `disputeable = false`, `resolve()` skips directly to `Finalized` — no dispute window, no `finalize()` call needed.
</Note>

## State Variables

| Variable              | Type                          | Description                                                                                     |
| --------------------- | ----------------------------- | ----------------------------------------------------------------------------------------------- |
| `factory`             | `IMouthBetFactory`            | MouthBetFactory address                                                                         |
| `usdc`                | `IERC20`                      | USDC token contract reference                                                                   |
| `challenger`          | `address`                     | Challenger's wallet address                                                                     |
| `opponent`            | `address`                     | Opponent's wallet for PvP (`address(0)` if Open PvP)                                            |
| `challengerAmount`    | `uint256`                     | USDC amount the Challenger deposits                                                             |
| `totalOpponentAmount` | `uint256`                     | Total USDC required from opponent(s), calculated from odds                                      |
| `odds`                | `uint256`                     | Challenger's perceived probability (10-90, default 50)                                          |
| `expirationDate`      | `uint256`                     | When the prediction should be evaluated                                                         |
| `acceptanceDeadline`  | `uint256`                     | Deadline for opponent(s) to accept (mandatory for all bet types)                                |
| `title`               | `string`                      | Short bet description                                                                           |
| `disputeable`         | `bool`                        | If `true`, 48h dispute window before claim. If `false`, immediately finalized after resolution. |
| `status`              | `BetStatus`                   | Current state of the bet                                                                        |
| `outcome`             | `BetOutcome`                  | Resolution outcome                                                                              |
| `resolvedAt`          | `uint256`                     | Timestamp when resolved                                                                         |
| `challengerDeposited` | `bool`                        | Whether Challenger has deposited                                                                |
| `filledAmount`        | `uint256`                     | Total USDC deposited by opponent(s) so far                                                      |
| `opponentDeposits`    | `mapping(address => uint256)` | Each opponent's deposit amount (Open PvP)                                                       |
| `opponentList`        | `address[]`                   | List of opponent addresses (Open PvP)                                                           |
| `claimableBalance`    | `uint256`                     | USDC balance snapshot taken at finalization                                                     |
| `hasClaimed`          | `mapping(address => bool)`    | Tracks which parties have claimed                                                               |
| `claimedCount`        | `uint256`                     | Number of parties that have claimed                                                             |
| `cancelApprovals`     | `mapping(address => bool)`    | Tracks mutual cancellation approvals                                                            |
| `feePercentage`       | `uint256`                     | Snapshot of factory fee at creation time (basis points)                                         |

### Constants

| Constant         | Value      | Description                                           |
| ---------------- | ---------- | ----------------------------------------------------- |
| `DISPUTE_WINDOW` | `48 hours` | Duration of the dispute window after resolution       |
| `MAX_OPPONENTS`  | `100`      | Maximum number of unique opponents in an Open PvP bet |

### Enums

```solidity theme={null}
enum BetStatus {
    Pending,      // Created, waiting for opponent(s)
    Filling,      // Open PvP: at least one opponent deposited, not fully filled
    Active,       // Fully funded and locked
    Resolved,     // Outcome determined, 48h dispute window active
    Finalized,    // Dispute window passed, claimable
    Claimed,      // Winner(s) claimed funds
    Cancelled,    // Challenger cancelled or mutual cancellation
    Expired       // Acceptance deadline passed with no fills
}

enum BetOutcome {
    Unresolved,
    ChallengerWins,
    OpponentWins,
    Draw
}
```

## Odds & Asymmetric Deposits

The `odds` parameter (10-90, default 50) determines the deposit ratio between the two sides:

* At **odds = 50**: both sides deposit the same amount (`challengerAmount == totalOpponentAmount`)
* At **other values**: deposits are asymmetric

**Formula:**

```
totalOpponentAmount = challengerAmount * (100 - odds) / odds
```

**Examples:**

| Odds | Challenger Deposits | Opponent(s) Must Deposit | Interpretation               |
| ---- | ------------------- | ------------------------ | ---------------------------- |
| 50   | \$500               | \$500                    | Even odds                    |
| 20   | \$100               | \$400                    | Challenger thinks 20% likely |
| 75   | \$750               | \$250                    | Challenger thinks 75% likely |

The 2% protocol fee is collected **per-deposit** — each opponent deposit triggers a proportional fee transfer to the treasury. The fee is calculated on the total pot: `fee = _depositAmount * (challengerAmount + totalOpponentAmount) * feePercentage / (totalOpponentAmount * 10000)`. For PvP 1v1, this happens in a single transaction when the opponent deposits. For Open PvP, each deposit triggers its share. The winner receives the entire remaining pot (both sides, net of fee), regardless of odds.

## Functions

### `deposit`

Opponent deposits USDC into the bet. The Challenger's deposit is handled by the Factory at creation time.

```solidity theme={null}
function deposit(uint256 _amount) external
```

**Logic (PvP 1v1):**

1. Caller must be the specified Opponent (Challenger cannot call — they deposit via Factory)
2. Bet must be in `Pending` status
3. Opponent deposits `totalOpponentAmount` (the `_amount` parameter is ignored, overridden to `totalOpponentAmount`)
4. Transfer USDC from caller to this contract (requires prior approval of the bet contract)
5. Collect 2% fee on total pot: `fee = (challengerAmount + totalOpponentAmount) * feePercentage / 10000`
6. Transfer fee to treasury
7. Status becomes `Active`
8. Emit `Deposited`, `FeeCollected`, and `BetActivated` events

**Logic (Open PvP):**

1. Caller must be any Mouth user (not the Challenger)
2. Bet must be in `Pending` or `Filling` status
3. `_amount` must be greater than 0 and not exceed remaining unfilled amount (`totalOpponentAmount - filledAmount`)
4. Transfer USDC from caller to this contract
5. Record deposit in `opponentDeposits[caller]` and add to `opponentList` if new (capped at `MAX_OPPONENTS = 100` unique depositors; existing depositors can top up without counting toward this limit)
6. Increment `filledAmount` by `_amount`
7. Status becomes `Filling` if not already
8. Collect proportional fee: `fee = _amount * (challengerAmount + totalOpponentAmount) * feePercentage / (totalOpponentAmount * 10000)`
9. Transfer fee to treasury
10. If `filledAmount == totalOpponentAmount`: Status becomes `Active`
11. Emit `Deposited` and `FeeCollected` events (and `BetActivated` event when fully filled)

<Note>
  The Challenger deposits their USDC via the Factory at bet creation time (`challengerDeposited` is set to `true` in `initialize`). The Challenger must approve the **Factory** contract, while opponents must approve the **bet** contract.
</Note>

### `activatePartial`

Activate an Open PvP bet with partial fills. Callable by the Challenger at any time during Filling, or by anyone after the acceptance deadline.

```solidity theme={null}
function activatePartial() external
```

**Logic:**

1. Bet must be in `Filling` status
2. Caller must be the Challenger, or `acceptanceDeadline` must have passed
3. `filledAmount` must be > 0
4. Recalculate `challengerAmount` proportionally: `matchedChallengerAmount = (filledAmount * challengerAmount) / totalOpponentAmount`
5. Return excess Challenger deposit (`challengerAmount - matchedChallengerAmount`) to Challenger
6. Update `challengerAmount` and `totalOpponentAmount` to the matched values
7. Status becomes `Active` (fee already collected per-deposit)
8. Emit `PartialActivation` and `BetActivated` events

### `withdraw`

Withdraw challenger deposit. Callable by the Challenger (anytime while Pending, before or after deadline) or by the Resolver (only after acceptance deadline for deserted bets).

```solidity theme={null}
function withdraw() external
```

**Logic:**

1. Caller must be the Challenger or the Resolver
2. Bet must be in `Pending` status (no opponent has deposited yet, or Open PvP with no fills)
3. Challenger must have deposited
4. If called by the Resolver, `acceptanceDeadline` must have passed (deserted bet cleanup)
5. If `acceptanceDeadline` has passed → set status to `Expired`, emit `BetExpired`
6. If called before deadline → set status to `Cancelled`
7. Transfer USDC back to Challenger, emit `Withdrawn` (emitted in all cases, regardless of timing)

### `resolve`

Submit the bet resolution. Only callable by the Factory's resolver.

```solidity theme={null}
function resolve(BetOutcome _outcome) external
```

**Logic:**

1. Caller must be the resolver (checked via Factory)
2. Bet must be in `Active` status
3. `block.timestamp` must be at or past `expirationDate`
4. Set outcome and `resolvedAt` timestamp
5. Emit `Resolved` event
6. **If `disputeable = true`**: set status to `Resolved` (48h dispute window starts)
7. **If `disputeable = false`**: snapshot `claimableBalance`, set status to `Finalized`, emit `Finalized` event (no dispute window)

### `reResolve`

Re-resolve during the 48-hour dispute window. Only callable by the resolver. Only for disputeable bets.

```solidity theme={null}
function reResolve(BetOutcome _outcome) external
```

**Logic:**

1. Bet must be `disputeable`
2. Caller must be the resolver (checked via Factory)
3. Bet must be in `Resolved` status
4. `block.timestamp` must be within the 48-hour dispute window (`< resolvedAt + DISPUTE_WINDOW`)
5. Update outcome and reset `resolvedAt` to current timestamp (restarts the 48h window)
6. Emit `ReResolved(oldOutcome, newOutcome)` event

<Note>
  `reResolve` is only available for disputeable bets. It allows the Mouth team to correct a resolution if a dispute is upheld. The 48-hour window resets after each re-resolution to give all parties time to review the new outcome.
</Note>

### `finalize`

Finalize the bet after the dispute window. Only relevant for disputeable bets (non-disputeable bets skip directly to `Finalized` in `resolve()`). Permissionless.

```solidity theme={null}
function finalize() external
```

**Logic:**

1. Bet must be in `Resolved` status
2. At least 48 hours must have passed since `resolvedAt`
3. Snapshot the contract's USDC balance into `claimableBalance`
4. Set status to `Finalized`
5. Emit `Finalized(claimableBalance)` event

### `claim`

Winner claims the pot.

```solidity theme={null}
function claim() external
```

The 2% protocol fee has already been collected at deposit time. The `claim` function simply distributes the remaining funds in the contract (stored in `claimableBalance`).

**Logic (PvP 1v1):**

1. Bet must be in `Finalized` status
2. Determine winner based on outcome
3. Distribute funds:
   * **ChallengerWins / OpponentWins**: transfer the entire remaining contract balance to the winner
   * **Draw**: each party receives their proportional share of the remaining balance
4. Set status to `Claimed`
5. Emit `Claimed` event

**Logic (Open PvP):**

1. Bet must be in `Finalized` status
2. Determine winner based on outcome
3. Distribute funds:
   * **ChallengerWins**: Challenger receives the entire remaining contract balance
   * **OpponentWins**: each opponent claims their proportional share of the remaining balance, based on their deposit ratio (`opponentDeposits[caller] / filledAmount`)
   * **Draw**: each party receives their proportional share of the remaining balance
4. Set status to `Claimed` (after all parties have claimed)
5. Emit `Claimed` event

### `resolverClaim`

Resolver-only batch claim: distributes all funds to winners in a single transaction.

```solidity theme={null}
function resolverClaim() external
```

**Logic:**

1. Caller must be the resolver (checked via Factory)
2. Bet must be in `Finalized` status
3. No individual `claim()` must have been called yet (`claimedCount == 0`) — once any party claims individually, `resolverClaim` is blocked
4. Distribute funds to all winners via `_distributeAll()`:
   * **ChallengerWins**: transfer entire `claimableBalance` to Challenger
   * **OpponentWins**: transfer proportional shares to each opponent (or full amount to single opponent in PvP)
   * **Draw**: transfer proportional shares to all parties
5. Set status to `Claimed`
6. Emit `Claimed` events for each recipient

<Note>
  `resolverClaim` is an alternative to individual `claim()` calls. For PvP 1v1 bets, the backend typically calls `resolverClaim` right after `resolve()` to distribute funds in one step. For Open PvP bets with many opponents, it may be safer to let opponents `claim()` individually to avoid out-of-gas issues.
</Note>

### `mutualCancel`

Both parties agree to cancel an active bet.

```solidity theme={null}
function mutualCancel() external
```

**Logic:**

1. Bet must be in `Active` status
2. Caller must be Challenger or Opponent (for 1v1) / any deposited opponent (for Open PvP)
3. Record caller's approval in `cancelApprovals`
4. For PvP: both Challenger and Opponent must have approved
5. For Open PvP: Challenger and all opponents must have approved
6. Refund each party their remaining deposit (the 2% fee was already collected at deposit time and is not refunded)
7. Set status to `Cancelled`
8. Emit `MutualCancelled` event

<Note>
  Mutual cancellation returns the remaining deposits to all parties. The 2% protocol fee was already collected at deposit time and is **not refunded**. A single party cannot unilaterally cancel an active bet.
</Note>

## Events

```solidity theme={null}
event Deposited(address indexed depositor, uint256 amount);
event FeeCollected(uint256 fee, address indexed treasury);
event Withdrawn(address indexed challenger, uint256 amount);
event BetActivated(uint256 totalPot);
event PartialActivation(uint256 filledAmount, uint256 returnedToChallenger);
event Resolved(BetOutcome outcome);
event ReResolved(BetOutcome oldOutcome, BetOutcome newOutcome);
event Finalized(uint256 claimableBalance);
event Claimed(address indexed claimant, uint256 payout);
event MutualCancelled();
event BetExpired();
```

## Access Control

| Function          | Who Can Call                                                                                      |
| ----------------- | ------------------------------------------------------------------------------------------------- |
| `deposit`         | Specified Opponent (PvP) or anyone except Challenger (Open PvP). Challenger deposits via Factory. |
| `activatePartial` | Challenger (anytime during Filling) or anyone (after acceptance deadline)                         |
| `withdraw`        | Challenger (anytime while Pending) or Resolver (after acceptance deadline for deserted bets)      |
| `resolve`         | Resolver only (via Factory)                                                                       |
| `reResolve`       | Resolver only (via Factory), during 48h dispute window. Only for disputeable bets.                |
| `finalize`        | Anyone (permissionless, just checks timing). Only for disputeable bets.                           |
| `claim`           | Winner only, or all parties in case of Draw (Open PvP: each opponent claims individually)         |
| `resolverClaim`   | Resolver only (via Factory). Batch distributes all funds.                                         |
| `mutualCancel`    | Challenger and Opponent(s) — all must approve                                                     |
