Skip to content

Instantly share code, notes, and snippets.

@coderwithsense
Created January 5, 2025 20:45
Show Gist options
  • Save coderwithsense/344296b94e4e42ee5b6f1141998f6ced to your computer and use it in GitHub Desktop.
Save coderwithsense/344296b94e4e42ee5b6f1141998f6ced to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.26+commit.8a97fa7a.js&optimize=false&runs=200&gist=
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
* @dev Minimal ERC20 interface
*/
interface IERC20 {
function transferFrom(
address sender,
address recipient,
uint256 amount
) external returns (bool);
function transfer(address recipient, uint256 amount)
external
returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function allowance(address owner, address spender)
external
view
returns (uint256);
function balanceOf(address account) external view returns (uint256);
}
/**
* @title MultiChoiceBetPool
* @notice A multi-bet contract with multiple-choice options, owner and creator fees,
* and handling for scenarios with no winners.
*/
contract MultiChoiceBetPool {
// ------------------------------------------------------------------------
// Constants & State Variables
// ------------------------------------------------------------------------
address public owner; // Contract owner
address public ownerWallet; // Owner wallet to receive global fees (5%)
IERC20 public token; // ERC20 token used for betting
uint256 public constant OWNER_FEE_BPS = 500; // 5% fee for the owner
uint256 public betCounter; // Bet ID counter
mapping(address => uint256) public allTimeEarnings; // Tracks total historical earnings per user across all bets
mapping(uint256 => mapping(address => UserBet)) public userBets; // Mapping: betId => (user address => UserBet)
mapping(uint256 => Bet) public bets; // Mapping: betId => Bet
// ------------------------------------------------------------------------
// Data Structures
// ------------------------------------------------------------------------
struct UserBet {
bool hasBet; // Has this user placed a bet on this betId
bool claimed; // Has the user claimed their reward
uint256 amount; // Amount of tokens user bet
uint256[] selections; // User's chosen options
}
struct Bet {
bool active; // Is betting open?
bool ended; // Is the bet ended?
uint256 totalPool; // Total tokens bet in this pool
uint256 feeAmount; // Owner fee withheld
uint256 creatorFeeAmount; // Creator fee withheld
uint256 netPool; // Net pool = totalPool - fees
uint256 totalWinningDeposits; // Sum of deposits from all winners
uint256 numOptions; // Number of multiple-choice options
uint256 maxSelected; // Maximum number of options a user can pick
bool[] correctOptions; // Correct options set during endBet
address creator; // Address of the creator
uint256 creatorFeeBPS; // Creator fee in basis points
address[] participants; // List of participants
}
struct BetInfo {
bool active;
bool ended;
uint256 totalPool;
uint256 feeAmount;
uint256 creatorFeeAmount;
uint256 netPool;
uint256 totalWinningDeposits;
uint256 numOptions;
uint256 maxSelected;
bool[] correctOptions;
address creator;
uint256 creatorFeeBPS;
}
// ------------------------------------------------------------------------
// Events
// ------------------------------------------------------------------------
event OwnerChanged(address indexed newOwner);
event OwnerWalletChanged(address indexed newOwnerWallet);
event TokenChanged(address indexed newToken);
event BetCreated(
uint256 indexed betId,
uint256 numOptions,
uint256 maxSelected,
address indexed creator,
uint256 creatorFeeBPS
);
event BetPlaced(uint256 indexed betId, address indexed user, uint256 amount, uint256[] selections);
event BetEnded(uint256 indexed betId, bool[] correctOptions, uint256 ownerFeeAmount, uint256 creatorFeeAmount, uint256 netPool);
event Claimed(uint256 indexed betId, address indexed user, uint256 reward);
event NoWinners(uint256 indexed betId, uint256 netPoolTransferred);
// ------------------------------------------------------------------------
// Modifiers
// ------------------------------------------------------------------------
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier betExists(uint256 betId) {
require(betId > 0 && betId <= betCounter, "Bet does not exist");
_;
}
// ------------------------------------------------------------------------
// Constructor
// ------------------------------------------------------------------------
constructor() {
owner = msg.sender;
ownerWallet = msg.sender;
}
// ------------------------------------------------------------------------
// Owner Functions
// ------------------------------------------------------------------------
function setOwner(address _newOwner) external onlyOwner {
require(_newOwner != address(0), "Zero address");
owner = _newOwner;
emit OwnerChanged(_newOwner);
}
function setOwnerWallet(address _newOwnerWallet) external onlyOwner {
require(_newOwnerWallet != address(0), "Zero address");
ownerWallet = _newOwnerWallet;
emit OwnerWalletChanged(_newOwnerWallet);
}
function setToken(address _token) external onlyOwner {
require(_token != address(0), "Zero address");
token = IERC20(_token);
emit TokenChanged(_token);
}
// ------------------------------------------------------------------------
// Bet Management
// ------------------------------------------------------------------------
function createBet(
uint256 _numOptions,
uint256 _maxSelected,
address _creator,
uint256 _creatorFeeBPS
) external onlyOwner returns (uint256 betId) {
require(_numOptions > 0, "Need at least 1 option");
require(_maxSelected > 0 && _maxSelected <= _numOptions, "Invalid maxSelected");
require(_creator != address(0), "Creator cannot be zero address");
require(_creatorFeeBPS <= 1000, "Creator fee too high");
betCounter++;
betId = betCounter;
Bet storage b = bets[betId];
b.active = true;
b.numOptions = _numOptions;
b.maxSelected = _maxSelected;
b.creator = _creator;
b.creatorFeeBPS = _creatorFeeBPS;
emit BetCreated(betId, _numOptions, _maxSelected, _creator, _creatorFeeBPS);
}
function placeBet(uint256 betId, uint256 amount, uint256[] calldata selections)
external
betExists(betId)
{
Bet storage b = bets[betId];
require(b.active, "Bet not active");
require(!b.ended, "Bet already ended");
require(amount > 0, "Amount = 0");
require(selections.length > 0 && selections.length <= b.maxSelected, "Invalid selections");
// Check if the user has approved enough tokens
uint256 allowance = token.allowance(msg.sender, address(this));
require(allowance >= amount, "Insufficient allowance, approve tokens first");
// Transfer tokens from user to the contract
bool ok = token.transferFrom(msg.sender, address(this), amount);
require(ok, "Transfer failed");
// Update bet data
b.totalPool += amount;
if (!userBets[betId][msg.sender].hasBet) {
b.participants.push(msg.sender);
}
// Update user bet data
UserBet storage ub = userBets[betId][msg.sender];
ub.hasBet = true;
ub.amount += amount;
ub.selections = selections;
emit BetPlaced(betId, msg.sender, amount, selections);
}
function endBet(uint256 betId, bool[] calldata _correctOptions) external onlyOwner betExists(betId) {
Bet storage b = bets[betId];
require(b.active, "Bet not active");
require(!b.ended, "Bet already ended");
require(_correctOptions.length == b.numOptions, "Mismatch length");
b.active = false;
b.ended = true;
b.correctOptions = _correctOptions;
uint256 ownerFee = (b.totalPool * OWNER_FEE_BPS) / 10000;
uint256 creatorFee = (b.totalPool * b.creatorFeeBPS) / 10000;
b.feeAmount = ownerFee;
b.creatorFeeAmount = creatorFee;
b.netPool = b.totalPool - ownerFee - creatorFee;
if (ownerFee > 0) token.transfer(ownerWallet, ownerFee);
if (creatorFee > 0) token.transfer(b.creator, creatorFee);
uint256 winnersDeposit = 0;
for (uint256 i = 0; i < b.participants.length; i++) {
address user = b.participants[i];
if (_isWinner(b, userBets[betId][user])) {
winnersDeposit += userBets[betId][user].amount;
}
}
b.totalWinningDeposits = winnersDeposit;
if (winnersDeposit == 0 && b.netPool > 0) {
token.transfer(ownerWallet, b.netPool);
emit NoWinners(betId, b.netPool);
} else {
emit BetEnded(betId, _correctOptions, ownerFee, creatorFee, b.netPool);
}
}
function claim(uint256 betId) external betExists(betId) {
UserBet storage ub = userBets[betId][msg.sender];
// Bet storage b = bets[betId];
uint256 claimAmount = checkClaimAmount(betId, msg.sender);
require(claimAmount > 0, "Nothing to claim");
ub.claimed = true;
allTimeEarnings[msg.sender] += claimAmount;
token.transfer(msg.sender, claimAmount);
emit Claimed(betId, msg.sender, claimAmount);
}
function checkClaimAmount(uint256 betId, address user) public view returns (uint256) {
Bet storage b = bets[betId];
UserBet storage ub = userBets[betId][user];
if (!b.ended || !ub.hasBet || ub.claimed) return 0;
if (!_isWinner(b, ub)) return 0;
if (b.totalWinningDeposits == 0) return 0;
return (ub.amount * b.netPool) / b.totalWinningDeposits;
}
function getBetInfo(uint256 betId) external view betExists(betId) returns (BetInfo memory) {
Bet storage b = bets[betId];
return BetInfo(
b.active,
b.ended,
b.totalPool,
b.feeAmount,
b.creatorFeeAmount,
b.netPool,
b.totalWinningDeposits,
b.numOptions,
b.maxSelected,
b.correctOptions,
b.creator,
b.creatorFeeBPS
);
}
function _isWinner(Bet storage b, UserBet storage ub) internal view returns (bool) {
uint256 correctCount = 0;
for (uint256 i = 0; i < b.correctOptions.length; i++) {
if (b.correctOptions[i]) correctCount++;
}
if (ub.selections.length != correctCount) return false;
for (uint256 i = 0; i < ub.selections.length; i++) {
if (!b.correctOptions[ub.selections[i]]) return false;
}
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment