Contract Name:
PrimapePrediction
Contract Source Code:
File 1 of 1 : PrimapePrediction
// File: @thirdweb-dev/contracts/extension/interface/IOwnable.sol
pragma solidity ^0.8.0;
/// @author thirdweb
/**
* Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading
* who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses
* information about who the contract's owner is.
*/
interface IOwnable {
/// @dev Returns the owner of the contract.
function owner() external view returns (address);
/// @dev Lets a module admin set a new owner for the contract. The new owner must be a module admin.
function setOwner(address _newOwner) external;
/// @dev Emitted when a new Owner is set.
event OwnerUpdated(address indexed prevOwner, address indexed newOwner);
}
// File: @thirdweb-dev/contracts/extension/Ownable.sol
pragma solidity ^0.8.0;
/// @author thirdweb
/**
* @title Ownable
* @notice Thirdweb's `Ownable` is a contract extension to be used with any base contract. It exposes functions for setting and reading
* who the 'owner' of the inheriting smart contract is, and lets the inheriting contract perform conditional logic that uses
* information about who the contract's owner is.
*/
abstract contract Ownable is IOwnable {
/// @dev The sender is not authorized to perform the action
error OwnableUnauthorized();
/// @dev Owner of the contract (purpose: OpenSea compatibility)
address private _owner;
/// @dev Reverts if caller is not the owner.
modifier onlyOwner() {
if (msg.sender != _owner) {
revert OwnableUnauthorized();
}
_;
}
/**
* @notice Returns the owner of the contract.
*/
function owner() public view override returns (address) {
return _owner;
}
/**
* @notice Lets an authorized wallet set a new owner for the contract.
* @param _newOwner The address to set as the new owner of the contract.
*/
function setOwner(address _newOwner) external override {
if (!_canSetOwner()) {
revert OwnableUnauthorized();
}
_setupOwner(_newOwner);
}
/// @dev Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.
function _setupOwner(address _newOwner) internal {
address _prevOwner = _owner;
_owner = _newOwner;
emit OwnerUpdated(_prevOwner, _newOwner);
}
/// @dev Returns whether owner can be set in the given execution context.
function _canSetOwner() internal view virtual returns (bool);
}
// File: @thirdweb-dev/contracts/external-deps/openzeppelin/security/ReentrancyGuard.sol
// OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol)
pragma solidity ^0.8.0;
abstract contract ReentrancyGuard {
uint256 private constant _NOT_ENTERED = 1;
uint256 private constant _ENTERED = 2;
uint256 private _status;
constructor() {
_status = _NOT_ENTERED;
}
/**
* @dev Prevents a contract from calling itself, directly or indirectly.
*/
modifier nonReentrant() {
// On the first call to nonReentrant, _notEntered will be true
require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
// Any calls to nonReentrant after this point will fail
_status = _ENTERED;
_;
// By storing the original value once again, a refund is triggered (see
// https://eips.ethereum.org/EIPS/eip-2200)
_status = _NOT_ENTERED;
}
}
// File: contracts/PrimapePredictions.sol
pragma solidity ^0.8.26;
/**
* @title PrimapePredictionNativeWithFee
* @dev A multi-outcome prediction market that uses the chain's native token for betting.
* Allows for early resolution and includes a platform fee adjustable by the owner.
*/
contract PrimapePrediction is Ownable, ReentrancyGuard {
struct Market {
string question;
uint256 endTime;
bool resolved;
uint256 winningOptionIndex; // If unresolved, set to type(uint256).max
}
uint256 public marketCount;
mapping(uint256 => Market) public markets;
// Market outcomes
mapping(uint256 => string[]) public marketOptions;
// Total shares per option: marketId => optionIndex => total amount staked
mapping(uint256 => mapping(uint256 => uint256)) public totalSharesPerOption;
// User shares per option: marketId => user => optionIndex => shares
mapping(uint256 => mapping(address => mapping(uint256 => uint256))) public userSharesPerOption;
// Track if a user has claimed winnings: marketId => user => bool
mapping(uint256 => mapping(address => bool)) public hasClaimed;
// Fee in basis points (BPS). 1% = 100 BPS, 0.5% = 50 BPS, etc.
uint256 public feeBps = 100; // Default 1% fee
// Accumulated platform fees
uint256 public platformBalance;
/// @notice Emitted when a new market is created.
event MarketCreated(
uint256 indexed marketId,
string question,
string[] options,
uint256 endTime
);
/// @notice Emitted when shares are purchased in a market.
event SharesPurchased(
uint256 indexed marketId,
address indexed buyer,
uint256 optionIndex,
uint256 amount
);
/// @notice Emitted when a market is resolved with a winning option.
event MarketResolved(uint256 indexed marketId, uint256 winningOptionIndex);
/// @notice Emitted when winnings are claimed by a user.
event Claimed(uint256 indexed marketId, address indexed user, uint256 amount);
/// @notice Emitted when the platform fee BPS is updated.
event FeeUpdated(uint256 newFeeBps);
/// @notice Emitted when platform fees are withdrawn.
event FeesWithdrawn(uint256 amount, address indexed recipient);
constructor() {
_setupOwner(msg.sender);
}
/**
* @dev Required override for Ownable extension.
* @return True if the caller is the contract owner.
*/
function _canSetOwner() internal view virtual override returns (bool) {
return msg.sender == owner();
}
/**
* @notice Creates a new prediction market with multiple outcomes.
* @param _question The question/prompt for the market.
* @param _options Array of outcome strings.
* @param _duration Duration in seconds for which the market is active.
* @return marketId ID of the newly created market.
*/
function createMarket(
string memory _question,
string[] memory _options,
uint256 _duration
) external returns (uint256) {
require(msg.sender == owner(), "Only owner can create markets");
require(_duration > 0, "Duration must be positive");
require(_options.length >= 2, "At least two outcomes required");
uint256 marketId = marketCount++;
Market storage market = markets[marketId];
market.question = _question;
market.endTime = block.timestamp + _duration;
market.resolved = false;
market.winningOptionIndex = type(uint256).max; // unresolved
for (uint256 i = 0; i < _options.length; i++) {
marketOptions[marketId].push(_options[i]);
}
emit MarketCreated(marketId, _question, _options, market.endTime);
return marketId;
}
/**
* @notice Buy shares in a specific option of a market using native token.
* @param _marketId The ID of the market.
* @param _optionIndex The index of the chosen outcome.
*/
function buyShares(
uint256 _marketId,
uint256 _optionIndex
) external payable {
Market storage market = markets[_marketId];
require(block.timestamp < market.endTime, "Market trading period ended");
require(!market.resolved, "Market already resolved");
require(_optionIndex < marketOptions[_marketId].length, "Invalid option");
require(msg.value > 0, "No funds sent");
// Calculate fee and net amount
uint256 feeAmount = (msg.value * feeBps) / 10000;
uint256 netAmount = msg.value - feeAmount;
platformBalance += feeAmount;
userSharesPerOption[_marketId][msg.sender][_optionIndex] += netAmount;
totalSharesPerOption[_marketId][_optionIndex] += netAmount;
emit SharesPurchased(_marketId, msg.sender, _optionIndex, netAmount);
}
/**
* @notice Resolve a market by specifying the winning option.
* @dev Owner can resolve early, no time check is enforced.
* @param _marketId The ID of the market to resolve.
* @param _winningOptionIndex The index of the winning outcome.
*/
function resolveMarket(
uint256 _marketId,
uint256 _winningOptionIndex
) external {
require(msg.sender == owner(), "Only owner can resolve");
Market storage market = markets[_marketId];
require(!market.resolved, "Already resolved");
require(_winningOptionIndex < marketOptions[_marketId].length, "Invalid outcome");
// Early resolution allowed. No requirement on block.timestamp.
market.winningOptionIndex = _winningOptionIndex;
market.resolved = true;
emit MarketResolved(_marketId, _winningOptionIndex);
}
/**
* @notice Claim winnings after a market is resolved.
* @param _marketId The ID of the market.
*/
function claimWinnings(uint256 _marketId) external nonReentrant {
Market storage market = markets[_marketId];
require(market.resolved, "Market not resolved");
require(!hasClaimed[_marketId][msg.sender], "Already claimed");
uint256 winningOption = market.winningOptionIndex;
uint256 userShares = userSharesPerOption[_marketId][msg.sender][winningOption];
require(userShares > 0, "No winnings");
uint256 winningShares = totalSharesPerOption[_marketId][winningOption];
uint256 losingShares;
uint256 optionCount = marketOptions[_marketId].length;
for (uint256 i = 0; i < optionCount; i++) {
if (i != winningOption) {
losingShares += totalSharesPerOption[_marketId][i];
}
}
// Calculate user's winnings: original stake + proportional share of losing pool
uint256 rewardRatio = 0;
if (winningShares > 0) {
rewardRatio = (losingShares * 1e18) / winningShares;
}
uint256 winnings = userShares + (userShares * rewardRatio) / 1e18;
// Reset user shares and mark claimed
userSharesPerOption[_marketId][msg.sender][winningOption] = 0;
hasClaimed[_marketId][msg.sender] = true;
(bool success, ) = payable(msg.sender).call{value: winnings}("");
require(success, "Transfer failed");
emit Claimed(_marketId, msg.sender, winnings);
}
/**
* @notice Batch claim winnings for multiple users.
* @param _marketId The ID of the market.
* @param _users The array of user addresses to claim for.
*/
function batchClaimWinnings(
uint256 _marketId,
address[] calldata _users
) external nonReentrant {
Market storage market = markets[_marketId];
require(market.resolved, "Market not resolved yet");
uint256 winningOption = market.winningOptionIndex;
uint256 winningShares = totalSharesPerOption[_marketId][winningOption];
uint256 losingShares;
uint256 optionCount = marketOptions[_marketId].length;
for (uint256 i = 0; i < optionCount; i++) {
if (i != winningOption) {
losingShares += totalSharesPerOption[_marketId][i];
}
}
uint256 rewardRatio = 0;
if (winningShares > 0) {
rewardRatio = (losingShares * 1e18) / winningShares;
}
for (uint256 i = 0; i < _users.length; i++) {
address user = _users[i];
if (hasClaimed[_marketId][user]) {
continue;
}
uint256 userShares = userSharesPerOption[_marketId][user][winningOption];
if (userShares == 0) {
continue;
}
uint256 winnings = userShares + (userShares * rewardRatio) / 1e18;
hasClaimed[_marketId][user] = true;
userSharesPerOption[_marketId][user][winningOption] = 0;
(bool success, ) = payable(user).call{value: winnings}("");
require(success, "Transfer failed");
emit Claimed(_marketId, user, winnings);
}
}
/**
* @notice Get the basic info of a market.
* @param _marketId The ID of the market.
* @return question The market's question.
* @return endTime The market's end time.
* @return resolved Whether the market has been resolved.
* @return winningOptionIndex The index of the winning option (or max uint if unresolved).
*/
function getMarketInfo(
uint256 _marketId
)
external
view
returns (
string memory question,
uint256 endTime,
bool resolved,
uint256 winningOptionIndex
)
{
Market storage market = markets[_marketId];
return (
market.question,
market.endTime,
market.resolved,
market.winningOptionIndex
);
}
/**
* @notice Get the options of a market.
* @param _marketId The ID of the market.
* @return An array of outcome option strings.
*/
function getMarketOptions(uint256 _marketId) external view returns (string[] memory) {
string[] memory opts = new string[](marketOptions[_marketId].length);
for (uint256 i = 0; i < marketOptions[_marketId].length; i++) {
opts[i] = marketOptions[_marketId][i];
}
return opts;
}
/**
* @notice Get user shares for each option in a market.
* @param _marketId The ID of the market.
* @param _user The user address.
* @return balances Array of user shares per option.
*/
function getUserShares(
uint256 _marketId,
address _user
) external view returns (uint256[] memory balances) {
uint256 optionCount = marketOptions[_marketId].length;
balances = new uint256[](optionCount);
for (uint256 i = 0; i < optionCount; i++) {
balances[i] = userSharesPerOption[_marketId][_user][i];
}
}
/**
* @notice Update the fee in basis points.
* @param _feeBps The new fee in basis points. For example, 100 = 1%.
*/
function setFeeBps(uint256 _feeBps) external onlyOwner {
require(_feeBps <= 1000, "Fee too high"); // max 10%
feeBps = _feeBps;
emit FeeUpdated(_feeBps);
}
/**
* @notice Owner can withdraw accumulated platform fees.
* @param _recipient The address to receive the withdrawn fees.
* @param _amount The amount of fees to withdraw (in wei).
*/
function withdrawFees(address payable _recipient, uint256 _amount) external onlyOwner nonReentrant {
require(_amount <= platformBalance, "Not enough fees");
platformBalance -= _amount;
(bool success, ) = _recipient.call{value: _amount}("");
require(success, "Withdraw failed");
emit FeesWithdrawn(_amount, _recipient);
}
}