Created
January 5, 2025 20:45
-
-
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=
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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