Skip to content

About

A UUPS upgradeable smart contract for binary prediction markets on Base (EVM). Users stake ERC-20 tokens on two outcomes, and winners receive their original stake plus a pro-rata share from the losing side pool.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Binary Betting Pool

A UUPS upgradeable smart contract for binary prediction markets on Base (EVM). Users stake ERC-20 tokens on two outcomes ("in favor" or "against"), and winners receive their original stake plus a pro-rata share from the losing side pool.

Overview

BinaryBettingPool is a decentralized betting protocol that enables:

  • Creation of multiple isolated betting pools within a single contract
  • Binary outcomes: users bet either "in favor" or "against" a proposition
  • Pro-rata payout distribution to winners
  • Protocol fee collection on resolution
  • Role-based access control (creator, resolver, admin)
  • Emergency pause functionality
  • Reentrancy protection with transient guards

Key Features

  • Multiple Pools: Create unlimited pools within a single contract instance
  • ERC-20 Native: Supports any ERC-20 token for betting stakes
  • Pool Lifecycle: Open → Locked → Resolved/Cancelled states
  • Pull-based Withdrawals: Winners and cancelled bettors claim via claimReward() and withdraw()
  • Flexible Fee Model: Configurable percentage fee deducted from losing pool only
  • Role-Based Access:
    • POOL_CREATOR_ROLE - create pools
    • POOL_RESOLVER_ROLE - lock, resolve, cancel pools
    • DEFAULT_ADMIN_ROLE - manage configuration and upgrades
  • UUPS Upgradeable: Future-proof protocol design with admin-controlled upgrades
  • Pausable: Emergency pause all user operations

Architecture

Core Structures

struct Pool {
    uint256 poolId;           // Unique pool identifier
    PoolState state;          // OPEN, LOCKED, RESOLVED, CANCELED
    uint256 stakeInFavor;     // Total tokens staked in favor
    uint256 stakeAgainst;     // Total tokens staked against
    uint8 winningOutcome;     // INFAVOR=1 or AGAINST=2 (after resolution)
    uint256 createdAt;        // Block timestamp of pool creation
    uint256 resolvedAt;       // Block timestamp of resolution (if resolved)
}

struct UserPosition {
    uint256 balanceInFavor;   // User's stake in favor
    uint256 balanceAgainst;   // User's stake against
    bool claimed;             // Has user claimed rewards/cancelled?
}

State Management

  • Per-pool user balances: userPositions[poolId][user] tracks each user's stakes
  • Available balance: userAvailableBalance[user] holds claimable funds across all pools
  • Pool registry: pools[poolId] stores pool state and stake totals

Pool Lifecycle

1. OPEN - Betting Phase

  • Users deposit stakes via depositBet(poolId, amount, inFavor)
  • Users can cancel their bet via cancelBet(poolId) (returns funds to available balance)
  • Pool creator can cancel the entire pool via cancelPool(poolId)

2. LOCKED - Awaiting Resolution

  • No further deposits or cancellations allowed
  • Pool enters this state via lockPool(poolId) (requires POOL_RESOLVER_ROLE)
  • Prepares for outcome determination

3. RESOLVED - Claims Open

  • Outcome is finalized via resolvePool(poolId, outcome) with winning side specified
  • Protocol fee deducted from losing pool, transferred to treasury
  • Winners claim rewards via claimReward(poolId)
  • Losers receive nothing (can cancel before locking to recover stakes)

4. CANCELED - Return Stakes

  • Pool cancelled at any point before resolution
  • Users withdraw stakes via cancelBet(poolId) (adds to available balance)

Payout Calculation

When a winner claims rewards:

winningStake = user's stake on winning side
totalWinningStake = sum of all stakes on winning side
totalLosingStake = sum of all stakes on losing side

reward = (winningStake / totalWinningStake) × totalLosingStake
payout = winningStake + reward

Protocol fee is deducted from totalLosingStake before distribution.

Usage

Deployment

// Deploy proxy + implementation via OpenZeppelin UUPS factory
// Then initialize:
bettingPool.initialize(tokenAddress, treasuryAddress);

// Grant roles
bettingPool.grantRole(POOL_CREATOR_ROLE, creatorAddress);
bettingPool.grantRole(POOL_RESOLVER_ROLE, resolverAddress);

Create and Bet

// Create a new pool
uint256 poolId = bettingPool.createPool();

// Place a bet in favor (amount in wei)
bettingPool.depositBet(poolId, amount, true);

// Place a bet against
bettingPool.depositBet(poolId, amount, false);

Lock and Resolve

// Lock pool (prevent further bets)
bettingPool.lockPool(poolId);

// Resolve with outcome (1 = INFAVOR, 2 = AGAINST)
bettingPool.resolvePool(poolId, 1);

Claim and Withdraw

// Winners claim rewards
bettingPool.claimReward(poolId);

// All users withdraw available balance
bettingPool.withdraw(amount);
// or withdraw entire balance
bettingPool.withdraw(type(uint256).max);

Configuration

Admin Functions

// Set minimum deposit required per bet
bettingPool.setMinDeposit(minAmount);

// Set protocol fee (in basis points: 100 = 1%)
bettingPool.setFee(250); // 2.5%

// Update treasury address
bettingPool.setTreasury(newTreasuryAddress);

// Pause/unpause all user operations
bettingPool.pause();
bettingPool.unpause();

Testing

Comprehensive test suite includes:

  • Unit tests: test/BinaryBettingPoolTest.t.sol
  • Invariant tests: test/invariant/
    • Handler-based fuzzing with Handler.t.sol
    • System invariants validated in Invariants.t.sol

Run tests with Foundry:

forge test
forge test --match-test invariant -vv

File Structure

src/
├── BinaryBettingPool.sol          # Main contract implementation
└── interfaces/
    └── IBinaryBettingPool.sol     # Contract interface & types

test/
├── BinaryBettingPoolTest.t.sol    # Unit tests
└── invariant/
    ├── Handler.t.sol              # Fuzzing handler
    └── Invariants.t.sol           # Invariant tests

Error Handling

Custom errors with context:

  • BinaryBettingPool_IncorrectPoolState - Pool not in expected state
  • BinaryBettingPool_InsufficientAmount - Bet below minimum or zero
  • BinaryBettingPool_InsufficientBalance - Insufficient available balance
  • BinaryBettingPool_PoolNotOpen - Pool not accepting bets
  • BinaryBettingPool_PoolNotLocked - Pool must be locked for resolution
  • BinaryBettingPool_PoolOneSideEmpty - Cannot resolve single-sided pool
  • BinaryBettingPool_UserPositionClaimed - User already claimed/cancelled this position
  • BinaryBettingPool_UserNotWinner - User not on winning side
  • BinaryBettingPool_InvalidOutcome - Invalid outcome value

Security Considerations

  • Reentrancy Guard: Using transient storage for gas efficiency
  • Safe ERC-20 Transfers: SafeERC20 for robust token interactions
  • Access Control: Role-based permissions for privileged operations
  • Pull-based Model: Users initiate withdrawals (no push to unknown addresses)
  • Input Validation: Checks for pool states, user positions, amounts

Dependencies

  • openzeppelin-contracts: IERC20, SafeERC20, Math utilities
  • openzeppelin-contracts-upgradeable: AccessControl, UUPS proxy, Pausable
  • forge-std: Foundry testing framework

License

MIT

About

A UUPS upgradeable smart contract for binary prediction markets on Base (EVM). Users stake ERC-20 tokens on two outcomes, and winners receive their original stake plus a pro-rata share from the losing side pool.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages