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.
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
- 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()andwithdraw() - Flexible Fee Model: Configurable percentage fee deducted from losing pool only
- Role-Based Access:
POOL_CREATOR_ROLE- create poolsPOOL_RESOLVER_ROLE- lock, resolve, cancel poolsDEFAULT_ADMIN_ROLE- manage configuration and upgrades
- UUPS Upgradeable: Future-proof protocol design with admin-controlled upgrades
- Pausable: Emergency pause all user operations
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?
}- 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
- 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)
- No further deposits or cancellations allowed
- Pool enters this state via
lockPool(poolId)(requires POOL_RESOLVER_ROLE) - Prepares for outcome determination
- 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)
- Pool cancelled at any point before resolution
- Users withdraw stakes via
cancelBet(poolId)(adds to available balance)
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.
// 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 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 pool (prevent further bets)
bettingPool.lockPool(poolId);
// Resolve with outcome (1 = INFAVOR, 2 = AGAINST)
bettingPool.resolvePool(poolId, 1);// Winners claim rewards
bettingPool.claimReward(poolId);
// All users withdraw available balance
bettingPool.withdraw(amount);
// or withdraw entire balance
bettingPool.withdraw(type(uint256).max);// 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();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
- Handler-based fuzzing with
Run tests with Foundry:
forge test
forge test --match-test invariant -vvsrc/
├── 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
Custom errors with context:
BinaryBettingPool_IncorrectPoolState- Pool not in expected stateBinaryBettingPool_InsufficientAmount- Bet below minimum or zeroBinaryBettingPool_InsufficientBalance- Insufficient available balanceBinaryBettingPool_PoolNotOpen- Pool not accepting betsBinaryBettingPool_PoolNotLocked- Pool must be locked for resolutionBinaryBettingPool_PoolOneSideEmpty- Cannot resolve single-sided poolBinaryBettingPool_UserPositionClaimed- User already claimed/cancelled this positionBinaryBettingPool_UserNotWinner- User not on winning sideBinaryBettingPool_InvalidOutcome- Invalid outcome value
- Reentrancy Guard: Using transient storage for gas efficiency
- Safe ERC-20 Transfers:
SafeERC20for 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
openzeppelin-contracts: IERC20, SafeERC20, Math utilitiesopenzeppelin-contracts-upgradeable: AccessControl, UUPS proxy, Pausableforge-std: Foundry testing framework
MIT