Skip to content

Instantly share code, notes, and snippets.

@uneeb123
Created December 6, 2024 02:15
Show Gist options
  • Save uneeb123/db7545f19ec24d441848f1c8095f7681 to your computer and use it in GitHub Desktop.
Save uneeb123/db7545f19ec24d441848f1c8095f7681 to your computer and use it in GitHub Desktop.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
interface ITokenBet {
/*
* ==================
* STRUCTS
* ==================
*/
struct Request {
address user;
address tokenAddress;
uint256 selections;
uint256 totalOutcomes;
uint256 betAmount;
}
struct TokenConfig {
uint24 feeTier; // Uniswap fee tier
bool isWhitelisted;
}
/*
* ==================
* EVENTS
* ==================
*/
event FundsUpdated(uint256 amount); // mostly for checking entropy balance
event ReservesUpdated(uint256 amount);
event ProtocolBalanceUpdated(uint256 amount);
event ReferrerBalanceUpdated(address indexed referrer, uint256 amount);
event CreditsUpdated(address indexed user, uint256 amount);
event Swap(
address indexed from,
address indexed to,
uint256 amountIn,
uint256 amountOut,
uint256 feesTier,
bool directionIn
);
event PlayRequest(
uint64 indexed sequenceNumber,
address indexed user,
address indexed tokenAddress,
uint256 selections,
uint256 wethAmount,
uint256 totalOutcomes,
uint256 amountIn,
address referrer,
uint256 protocolFee,
uint256 referrerFee
);
event PlayResult(
uint64 indexed sequenceNumber,
address indexed user,
address indexed tokenAddress,
uint256 outcome,
uint256 winAmount,
uint256 multiplier
);
/*
* ==================
* FUNCTIONS
* ==================
*/
function flip(
address tokenIn,
uint256 amountIn,
uint256 selection,
bytes32 userRandomNumber,
address referrer
) external;
function play(
address tokenAddress,
uint256 amountIn,
uint256 selections,
uint256 totalOutcomes,
bytes32 userRandomNumber,
address referrer
) external;
function playNoReferrer(
address tokenIn,
uint256 amountIn,
uint256 selections,
uint256 totalOutcomes,
bytes32 userRandomNumber
) external;
function playWithCredits(
uint256 amountIn,
uint256 selections,
uint256 totalOutcomes,
bytes32 userRandomNumber
) external;
function claimReferrerFees() external;
function depositReserves() external payable;
function withdrawReserves(uint256 value) external;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.23;
import {IEntropyConsumer} from "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol";
import {IEntropy} from "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {ITokenBet} from "./interface/ITokenBet.sol";
import {ISwapRouter02} from "./interface/ISwapRouter02.sol";
import {IWETH} from "./interface/IWETH.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
/**
* @title TokenBet Contract
* @author asdf
* @notice A decentralized betting contract that accepts ERC20 tokens as wagers
* @dev Implementation of a betting system with the following features:
* - Uses Pyth Network's Entropy service for verifiable randomness
* - Converts input tokens to WETH via Uniswap V3 for standardized betting
* - Includes configurable protocol and referral fees
* - Maintains reserves in WETH to cover potential payouts
* - Supports multiple betting modes (binary/multi-outcome)
* - Implements checks-effects-interactions pattern for security
* - Allows for token whitelisting and fee tier configuration
* - Supports both direct token bets and credit-based betting
*/
contract TokenBet is IEntropyConsumer, Ownable, ITokenBet {
ISwapRouter02 public immutable router;
IEntropy public immutable entropy;
IWETH public immutable weth;
uint256 public reserves; // denominated in WETH
uint256 public entropyFunds;
address public entropyProvider;
mapping(address => TokenConfig) public tokenConfigs;
uint256 public protocolFeeBps;
uint256 public referrerFeeBps;
mapping(address => uint256) public referrerBalances;
uint256 public protocolBalance;
mapping(address => uint256) public userCredits;
uint256 public maxPayoutMultiplier;
// maps sequence number to flip request (required by entropy)
mapping(uint256 => Request) private allRequests;
constructor(
address _entropy,
address _weth9,
address _router
) Ownable(msg.sender) {
entropy = IEntropy(_entropy);
entropyProvider = entropy.getDefaultProvider();
weth = IWETH(_weth9);
router = ISwapRouter02(_router);
protocolFeeBps = 50; // 0.5% for the protocol
referrerFeeBps = 450; // 4.5% for the referrer
maxPayoutMultiplier = 5; // 5x multiplier
}
/*
* =============================
* LIQUIDITY PROVISIONING
* =============================
*/
/**
* @notice Deposits ETH into the contract, converting it to WETH and increasing reserves.
*/
function depositReserves() external payable {
weth.deposit{value: msg.value}();
_incrementReserves(msg.value);
}
// Allows the contract to receive ETH
receive() external payable {
entropyFunds += msg.value;
emit FundsUpdated(entropyFunds);
}
/**
* @notice Withdraws a specified amount of WETH from the reserves, converting it to ETH.
* @param value The amount of WETH to withdraw.
*/
function withdrawReserves(uint256 value) external onlyOwner {
weth.withdraw(value);
_decrementReserves(value);
_transferETH(owner(), value);
}
/**
* @notice Withdraws a specified amount of ETH from the contract.
* @param amount The amount of ETH to withdraw.
*/
function withdrawETH(uint256 amount) external onlyOwner {
entropyFunds -= amount;
_transferETH(owner(), amount);
emit FundsUpdated(entropyFunds);
}
/**
* @notice Withdraws a specified amount of a given ERC20 token from the contract.
* @param token The address of the ERC20 token.
* @param amount The amount of the token to withdraw.
*/
function withdrawToken(address token, uint256 amount) external onlyOwner {
IERC20(token).transfer(owner(), amount);
}
/*
* ===========================
* USER FUNCTIONALITIES
* ===========================
*/
/**
* @notice Initiates a binary (coin flip) bet with a specified ERC20 token.
* @param tokenIn The address of the ERC20 token used for the bet.
* @param amountIn The amount of the token to bet.
* @param selection The user's selection (1 or 2) for the binary outcome.
* @param userRandomNumber A random number provided by the user.
* @param referrer The address of the referrer for fee distribution.
*/
function flip(
address tokenIn,
uint256 amountIn,
uint256 selection,
bytes32 userRandomNumber,
address referrer
) external {
// For a coin flip, we have 2 outcomes
uint256 totalOutcomes = 2;
// Ensure selection is either 1 or 2 (binary 01 or 10)
require(selection == 1 || selection == 2, "Invalid selection for flip");
_play(
tokenIn,
amountIn,
selection,
totalOutcomes,
userRandomNumber,
referrer
);
}
/**
* @notice Initiates a bet with multiple possible outcomes using a specified ERC20 token.
* @param tokenIn The ERC20 address to use (address(0) for credits)
* @param amountIn The amount of the token to bet.
* @param selections Bitmap of selected outcomes (e.g., 5 = binary 101 means selecting outcomes 0 and 2)
* @param totalOutcomes The total number of possible outcomes.
* @param userRandomNumber A random number provided by the user (used by Entropy).
* @param referrer The address of the referrer for fee distribution.
*/
function play(
address tokenIn,
uint256 amountIn,
uint256 selections,
uint256 totalOutcomes,
bytes32 userRandomNumber,
address referrer
) external {
_play(
tokenIn,
amountIn,
selections,
totalOutcomes,
userRandomNumber,
referrer
);
}
/**
* @notice Initiates a bet with multiple possible outcomes using a specified ERC20 token, without a referrer.
* @param tokenIn The ERC20 address to use (address(0) for credits)
* @param amountIn The amount of the token to bet.
* @param selections Bitmap of selected outcomes (e.g., 5 = binary 101 means selecting outcomes 0 and 2)
* @param totalOutcomes The total number of possible outcomes.
* @param userRandomNumber A random number provided by the user (used by Entropy).
*/
function playNoReferrer(
address tokenIn,
uint256 amountIn,
uint256 selections,
uint256 totalOutcomes,
bytes32 userRandomNumber
) external {
_play(
tokenIn,
amountIn,
selections,
totalOutcomes,
userRandomNumber,
address(0)
);
}
/**
* @notice Initiates a bet using user credits instead of ERC20 tokens.
* @param amountIn The amount of credits to bet.
* @param selections Bitmap of selected outcomes (e.g., 5 = binary 101 means selecting outcomes 0 and 2)
* @param totalOutcomes The total number of possible outcomes.
* @param userRandomNumber A random number provided by the user.
*/
function playWithCredits(
uint256 amountIn,
uint256 selections,
uint256 totalOutcomes,
bytes32 userRandomNumber
) external {
_play(
address(0),
amountIn,
selections,
totalOutcomes,
userRandomNumber,
address(0)
);
}
/**
* @notice Claims accumulated referrer fees in WETH.
*/
function claimReferrerFees() external {
uint256 amount = referrerBalances[msg.sender];
require(amount > 0, "No fees to claim");
// Update state before transfer
referrerBalances[msg.sender] = 0;
// Transfer WETH to referrer
weth.transfer(msg.sender, amount);
emit ReferrerBalanceUpdated(msg.sender, 0);
}
/*
* =====================
* ADMIN CONTROLS
* =====================
*/
/**
* @notice Sets the protocol fee in basis points.
* @param feeBps The new protocol fee in basis points.
*/
function setProtocolFeeBps(uint256 feeBps) public onlyOwner {
protocolFeeBps = feeBps;
}
/**
* @notice Sets the referrer fee in basis points.
* @param feeBps The new referrer fee in basis points.
*/
function setReferrerFeeBps(uint256 feeBps) public onlyOwner {
referrerFeeBps = feeBps;
}
/**
* @notice Configures a token's fee tier and whitelist status.
* @param token The address of the ERC20 token.
* @param fee The fee tier for the token.
* @param isWhitelisted Whether the token is whitelisted for betting.
*/
function setTokenConfig(
address token,
uint24 fee,
bool isWhitelisted
) public onlyOwner {
tokenConfigs[token] = TokenConfig({
feeTier: fee,
isWhitelisted: isWhitelisted
});
}
/**
* @notice Withdraws protocol fees in WETH.
* @param amount The amount of WETH to withdraw.
*/
function withdrawProtocolFees(uint256 amount) external onlyOwner {
protocolBalance -= amount;
weth.withdraw(amount);
_transferETH(owner(), amount);
emit ProtocolBalanceUpdated(protocolBalance);
}
/**
* @notice Credits a user with a specified amount of credits.
* @param user The address of the user to credit.
* @param amount The amount of credits to add.
*/
function creditUser(address user, uint256 amount) external onlyOwner {
userCredits[user] += amount;
emit CreditsUpdated(user, userCredits[user]);
}
/**
* @notice Sets the maximum payout multiplier (in 18 decimals)
* @param multiplier The new max multiplier (e.g., 5e18 for 5x)
*/
function setMaxPayoutMultiplier(uint256 multiplier) external onlyOwner {
require(multiplier > 1, "Multiplier must be greater than 1x");
maxPayoutMultiplier = multiplier;
}
/*
* ===================
* INTERNAL
* ===================
*/
// Get the fee to flip a coin. See the comment above about fees.
function getFlipFee() internal view returns (uint256 fee) {
fee = entropy.getFee(entropyProvider);
}
// This method is required by the IEntropyConsumer interface.
// It returns the address of the entropy contract which will call the callback.
function getEntropy() internal view override returns (address) {
return address(entropy);
}
// This method is required by the IEntropyConsumer interface.
// It is called by the entropy contract when a random number is generated.
function entropyCallback(
uint64 sequenceNumber,
// entropy provider not used
address,
bytes32 randomNumber
) internal override {
// Determine if the user won or not
Request memory req = allRequests[sequenceNumber];
// Calculate outcome using modulo of total outcomes
uint256 outcome = uint256(randomNumber) % req.totalOutcomes;
// Check if the outcome is one of the selected ones using bitmap
bool userWon = (req.selections & (1 << outcome)) != 0;
// Calculate payout if won
uint256 selectedCount = _countSetBits(req.selections);
uint256 payoutMultiplier = (req.totalOutcomes * 1e18) / selectedCount;
uint256 winning = userWon
? ((req.betAmount * payoutMultiplier) / 1e18)
: 0;
if (userWon) {
_convertAndTransfer(req.tokenAddress, req.user, winning);
_decrementReserves(winning - req.betAmount);
emit PlayResult(
sequenceNumber,
req.user,
req.tokenAddress,
outcome,
winning,
payoutMultiplier
);
} else {
_incrementReserves(req.betAmount);
emit PlayResult(
sequenceNumber,
req.user,
req.tokenAddress,
outcome,
0,
0
);
}
// Remove the entry after processing
delete allRequests[sequenceNumber];
}
/*
* =====================
* PRIVATE
* =====================
*/
// Core functionality for playing
function _play(
address tokenIn,
uint256 amountIn,
uint256 selections,
uint256 totalOutcomes,
bytes32 userRandomNumber,
address referrer
) private {
// Move some calculations into a separate function to reduce stack variables
(
uint256 betAmount,
uint256 protocolFee,
uint256 referrerFee
) = _calculateFeesAndBetAmount(tokenIn, amountIn, referrer);
// Validate inputs and check multiplier
_validatePlayInputs(selections, totalOutcomes, betAmount);
// Request random number and store request
uint64 sequenceNumber = _storePlayRequest(
tokenIn,
selections,
betAmount,
totalOutcomes,
userRandomNumber
);
// Emit the play request event
emit PlayRequest(
sequenceNumber,
msg.sender,
tokenIn,
selections,
betAmount,
totalOutcomes,
amountIn,
referrer,
protocolFee,
referrerFee
);
}
function _calculateFeesAndBetAmount(
address tokenIn,
uint256 amountIn,
address referrer
)
private
returns (uint256 betAmount, uint256 protocolFee, uint256 referrerFee)
{
uint256 wethAmount;
if (tokenIn == address(0)) {
require(
userCredits[msg.sender] >= amountIn,
"Insufficient credits"
);
userCredits[msg.sender] -= amountIn;
emit CreditsUpdated(msg.sender, userCredits[msg.sender]);
wethAmount = amountIn;
} else {
wethAmount = _swapTokensForWETH(tokenIn, amountIn);
}
uint256 totalFeeBps = protocolFeeBps + referrerFeeBps;
uint256 totalFee = (wethAmount * totalFeeBps) / 10_000;
protocolFee = (wethAmount * protocolFeeBps) / 10_000;
referrerFee = totalFee - protocolFee;
betAmount = wethAmount - totalFee;
_handleFees(referrer, protocolFee, referrerFee);
return (betAmount, protocolFee, referrerFee);
}
function _validatePlayInputs(
uint256 selections,
uint256 totalOutcomes,
uint256 betAmount
) private view {
require(totalOutcomes > 1, "Must have multiple outcomes");
require(selections > 0, "Must select at least one outcome");
require(selections < (1 << totalOutcomes), "Selection out of range");
uint256 selectedCount = _countSetBits(selections);
require(selectedCount < totalOutcomes, "Cannot select all outcomes");
uint256 payoutMultiplier = (totalOutcomes * 1e18) / selectedCount;
require(
payoutMultiplier <= maxPayoutMultiplier * 1e18,
"Payout multiplier too high"
);
uint256 maxPayout = (betAmount * payoutMultiplier) / 1e18;
require(
reserves >= maxPayout,
"Not enough reserves to cover the maximum payout"
);
}
function _storePlayRequest(
address tokenIn,
uint256 selections,
uint256 betAmount,
uint256 totalOutcomes,
bytes32 userRandomNumber
) private returns (uint64) {
uint256 fee = entropy.getFee(entropyProvider);
uint64 sequenceNumber = entropy.requestWithCallback{value: fee}(
entropyProvider,
userRandomNumber
);
entropyFunds -= fee;
emit FundsUpdated(entropyFunds);
Request memory newRequest = Request({
user: msg.sender,
tokenAddress: tokenIn,
selections: selections,
betAmount: betAmount,
totalOutcomes: totalOutcomes
});
allRequests[sequenceNumber] = newRequest;
return sequenceNumber;
}
function _handleFees(
address referrer,
uint256 protocolFee,
uint256 referrerFee
) private {
if (referrer != address(0)) {
referrerBalances[referrer] += referrerFee;
emit ReferrerBalanceUpdated(referrer, referrerBalances[referrer]);
} else {
protocolFee += referrerFee;
}
protocolBalance += protocolFee;
emit ProtocolBalanceUpdated(protocolBalance);
}
function _swapTokensForWETH(
address tokenIn,
uint256 amountIn
) private returns (uint256) {
require(
tokenConfigs[tokenIn].isWhitelisted,
"Token is not whitelisted"
);
IERC20(tokenIn).transferFrom(msg.sender, address(this), amountIn);
IERC20(tokenIn).approve(address(router), amountIn);
ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02
.ExactInputSingleParams({
tokenIn: tokenIn,
tokenOut: address(weth),
fee: tokenConfigs[tokenIn].feeTier,
recipient: address(this),
amountIn: amountIn,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
uint256 wethAmount = router.exactInputSingle(params);
emit Swap(
tokenIn,
address(weth),
amountIn,
wethAmount,
tokenConfigs[tokenIn].feeTier,
true
);
return wethAmount;
}
// Counts the number of set bits in a uint256
function _countSetBits(uint256 n) private pure returns (uint256 count) {
while (n != 0) {
count += n & 1;
n >>= 1;
}
return count;
}
function _convertAndTransfer(
address tokenAddress,
address recipient,
uint256 amount
) private {
if (tokenAddress == address(0)) {
// Using credits
weth.transfer(recipient, amount);
} else {
// Approve Router to spend tokens
weth.approve(address(router), amount);
// Swap WETH for tokens
ISwapRouter02.ExactInputSingleParams memory params = ISwapRouter02
.ExactInputSingleParams({
tokenIn: address(weth),
tokenOut: tokenAddress,
fee: tokenConfigs[tokenAddress].feeTier,
recipient: recipient,
amountIn: amount,
amountOutMinimum: 0,
sqrtPriceLimitX96: 0
});
uint256 amountOut = router.exactInputSingle(params);
emit Swap(
address(weth),
tokenAddress,
amount,
amountOut,
tokenConfigs[tokenAddress].feeTier,
false
);
}
}
function _incrementReserves(uint256 value) private {
reserves += value;
emit ReservesUpdated(reserves);
}
function _decrementReserves(uint256 value) private {
reserves -= value;
emit ReservesUpdated(reserves);
}
// also, ensure that interactions happen at the end
// https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html
function _transferETH(address receiver, uint256 amount) private {
// call is preferred over send / transfer
// https://consensys.io/diligence/blog/2019/09/stop-using-soliditys-transfer-now/
(bool success, ) = payable(receiver).call{value: amount}("");
require(success, "Transfer failed");
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment