Created
December 6, 2024 02:15
-
-
Save uneeb123/db7545f19ec24d441848f1c8095f7681 to your computer and use it in GitHub Desktop.
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.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; | |
} |
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.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