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/extension/interface/IPermissions.sol
pragma solidity ^0.8.0;
/// @author thirdweb
/**
* @dev External interface of AccessControl declared to support ERC165 detection.
*/
interface IPermissions {
/**
* @dev Emitted when `newAdminRole` is set as ``role``'s admin role, replacing `previousAdminRole`
*
* `DEFAULT_ADMIN_ROLE` is the starting admin for all roles, despite
* {RoleAdminChanged} not being emitted signaling this.
*
* _Available since v3.1._
*/
event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole);
/**
* @dev Emitted when `account` is granted `role`.
*
* `sender` is the account that originated the contract call, an admin role
* bearer except when using {AccessControl-_setupRole}.
*/
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Emitted when `account` is revoked `role`.
*
* `sender` is the account that originated the contract call:
* - if using `revokeRole`, it is the admin role bearer
* - if using `renounceRole`, it is the role bearer (i.e. `account`)
*/
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);
/**
* @dev Returns `true` if `account` has been granted `role`.
*/
function hasRole(bytes32 role, address account) external view returns (bool);
/**
* @dev Returns the admin role that controls `role`. See {grantRole} and
* {revokeRole}.
*
* To change a role's admin, use {AccessControl-_setRoleAdmin}.
*/
function getRoleAdmin(bytes32 role) external view returns (bytes32);
/**
* @dev Grants `role` to `account`.
*
* If `account` had not been already granted `role`, emits a {RoleGranted}
* event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function grantRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from `account`.
*
* If `account` had been granted `role`, emits a {RoleRevoked} event.
*
* Requirements:
*
* - the caller must have ``role``'s admin role.
*/
function revokeRole(bytes32 role, address account) external;
/**
* @dev Revokes `role` from the calling account.
*
* Roles are often managed via {grantRole} and {revokeRole}: this function's
* purpose is to provide a mechanism for accounts to lose their privileges
* if they are compromised (such as when a trusted device is misplaced).
*
* If the calling account had been granted `role`, emits a {RoleRevoked}
* event.
*
* Requirements:
*
* - the caller must be `account`.
*/
function renounceRole(bytes32 role, address account) external;
}
// File: @thirdweb-dev/contracts/lib/Strings.sol
pragma solidity ^0.8.0;
/// @author thirdweb
/**
* @dev String operations.
*/
library Strings {
bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
/**
* @dev Converts a `uint256` to its ASCII `string` decimal representation.
*/
function toString(uint256 value) internal pure returns (string memory) {
// Inspired by OraclizeAPI's implementation - MIT licence
// https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
if (value == 0) {
return "0";
}
uint256 temp = value;
uint256 digits;
while (temp != 0) {
digits++;
temp /= 10;
}
bytes memory buffer = new bytes(digits);
while (value != 0) {
digits -= 1;
buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
value /= 10;
}
return string(buffer);
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
*/
function toHexString(uint256 value) internal pure returns (string memory) {
if (value == 0) {
return "0x00";
}
uint256 temp = value;
uint256 length = 0;
while (temp != 0) {
length++;
temp >>= 8;
}
return toHexString(value, length);
}
/**
* @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
*/
function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
bytes memory buffer = new bytes(2 * length + 2);
buffer[0] = "0";
buffer[1] = "x";
for (uint256 i = 2 * length + 1; i > 1; --i) {
buffer[i] = _HEX_SYMBOLS[value & 0xf];
value >>= 4;
}
require(value == 0, "Strings: hex length insufficient");
return string(buffer);
}
/// @dev Returns the hexadecimal representation of `value`.
/// The output is prefixed with "0x", encoded using 2 hexadecimal digits per byte,
/// and the alphabets are capitalized conditionally according to
/// https://eips.ethereum.org/EIPS/eip-55
function toHexStringChecksummed(address value) internal pure returns (string memory str) {
str = toHexString(value);
/// @solidity memory-safe-assembly
assembly {
let mask := shl(6, div(not(0), 255)) // `0b010000000100000000 ...`
let o := add(str, 0x22)
let hashed := and(keccak256(o, 40), mul(34, mask)) // `0b10001000 ... `
let t := shl(240, 136) // `0b10001000 << 240`
for {
let i := 0
} 1 {
} {
mstore(add(i, i), mul(t, byte(i, hashed)))
i := add(i, 1)
if eq(i, 20) {
break
}
}
mstore(o, xor(mload(o), shr(1, and(mload(0x00), and(mload(o), mask)))))
o := add(o, 0x20)
mstore(o, xor(mload(o), shr(1, and(mload(0x20), and(mload(o), mask)))))
}
}
/// @dev Returns the hexadecimal representation of `value`.
/// The output is prefixed with "0x" and encoded using 2 hexadecimal digits per byte.
function toHexString(address value) internal pure returns (string memory str) {
str = toHexStringNoPrefix(value);
/// @solidity memory-safe-assembly
assembly {
let strLength := add(mload(str), 2) // Compute the length.
mstore(str, 0x3078) // Write the "0x" prefix.
str := sub(str, 2) // Move the pointer.
mstore(str, strLength) // Write the length.
}
}
/// @dev Returns the hexadecimal representation of `value`.
/// The output is encoded using 2 hexadecimal digits per byte.
function toHexStringNoPrefix(address value) internal pure returns (string memory str) {
/// @solidity memory-safe-assembly
assembly {
str := mload(0x40)
// Allocate the memory.
// We need 0x20 bytes for the trailing zeros padding, 0x20 bytes for the length,
// 0x02 bytes for the prefix, and 0x28 bytes for the digits.
// The next multiple of 0x20 above (0x20 + 0x20 + 0x02 + 0x28) is 0x80.
mstore(0x40, add(str, 0x80))
// Store "0123456789abcdef" in scratch space.
mstore(0x0f, 0x30313233343536373839616263646566)
str := add(str, 2)
mstore(str, 40)
let o := add(str, 0x20)
mstore(add(o, 40), 0)
value := shl(96, value)
// We write the string from rightmost digit to leftmost digit.
// The following is essentially a do-while loop that also handles the zero case.
for {
let i := 0
} 1 {
} {
let p := add(o, add(i, i))
let temp := byte(i, value)
mstore8(add(p, 1), mload(and(temp, 15)))
mstore8(p, mload(shr(4, temp)))
i := add(i, 1)
if eq(i, 20) {
break
}
}
}
}
/// @dev Returns the hex encoded string from the raw bytes.
/// The output is encoded using 2 hexadecimal digits per byte.
function toHexString(bytes memory raw) internal pure returns (string memory str) {
str = toHexStringNoPrefix(raw);
/// @solidity memory-safe-assembly
assembly {
let strLength := add(mload(str), 2) // Compute the length.
mstore(str, 0x3078) // Write the "0x" prefix.
str := sub(str, 2) // Move the pointer.
mstore(str, strLength) // Write the length.
}
}
/// @dev Returns the hex encoded string from the raw bytes.
/// The output is encoded using 2 hexadecimal digits per byte.
function toHexStringNoPrefix(bytes memory raw) internal pure returns (string memory str) {
/// @solidity memory-safe-assembly
assembly {
let length := mload(raw)
str := add(mload(0x40), 2) // Skip 2 bytes for the optional prefix.
mstore(str, add(length, length)) // Store the length of the output.
// Store "0123456789abcdef" in scratch space.
mstore(0x0f, 0x30313233343536373839616263646566)
let o := add(str, 0x20)
let end := add(raw, length)
for {
} iszero(eq(raw, end)) {
} {
raw := add(raw, 1)
mstore8(add(o, 1), mload(and(mload(raw), 15)))
mstore8(o, mload(and(shr(4, mload(raw)), 15)))
o := add(o, 2)
}
mstore(o, 0) // Zeroize the slot after the string.
mstore(0x40, add(o, 0x20)) // Allocate the memory.
}
}
}
// File: @thirdweb-dev/contracts/extension/Permissions.sol
pragma solidity ^0.8.0;
/// @author thirdweb
/**
* @title Permissions
* @dev This contracts provides extending-contracts with role-based access control mechanisms
*/
contract Permissions is IPermissions {
/// @dev The `account` is missing a role.
error PermissionsUnauthorizedAccount(address account, bytes32 neededRole);
/// @dev The `account` already is a holder of `role`
error PermissionsAlreadyGranted(address account, bytes32 role);
/// @dev Invalid priviledge to revoke
error PermissionsInvalidPermission(address expected, address actual);
/// @dev Map from keccak256 hash of a role => a map from address => whether address has role.
mapping(bytes32 => mapping(address => bool)) private _hasRole;
/// @dev Map from keccak256 hash of a role to role admin. See {getRoleAdmin}.
mapping(bytes32 => bytes32) private _getRoleAdmin;
/// @dev Default admin role for all roles. Only accounts with this role can grant/revoke other roles.
bytes32 public constant DEFAULT_ADMIN_ROLE = 0x00;
/// @dev Modifier that checks if an account has the specified role; reverts otherwise.
modifier onlyRole(bytes32 role) {
_checkRole(role, msg.sender);
_;
}
/**
* @notice Checks whether an account has a particular role.
* @dev Returns `true` if `account` has been granted `role`.
*
* @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE")
* @param account Address of the account for which the role is being checked.
*/
function hasRole(bytes32 role, address account) public view override returns (bool) {
return _hasRole[role][account];
}
/**
* @notice Checks whether an account has a particular role;
* role restrictions can be swtiched on and off.
*
* @dev Returns `true` if `account` has been granted `role`.
* Role restrictions can be swtiched on and off:
* - If address(0) has ROLE, then the ROLE restrictions
* don't apply.
* - If address(0) does not have ROLE, then the ROLE
* restrictions will apply.
*
* @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE")
* @param account Address of the account for which the role is being checked.
*/
function hasRoleWithSwitch(bytes32 role, address account) public view returns (bool) {
if (!_hasRole[role][address(0)]) {
return _hasRole[role][account];
}
return true;
}
/**
* @notice Returns the admin role that controls the specified role.
* @dev See {grantRole} and {revokeRole}.
* To change a role's admin, use {_setRoleAdmin}.
*
* @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE")
*/
function getRoleAdmin(bytes32 role) external view override returns (bytes32) {
return _getRoleAdmin[role];
}
/**
* @notice Grants a role to an account, if not previously granted.
* @dev Caller must have admin role for the `role`.
* Emits {RoleGranted Event}.
*
* @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE")
* @param account Address of the account to which the role is being granted.
*/
function grantRole(bytes32 role, address account) public virtual override {
_checkRole(_getRoleAdmin[role], msg.sender);
if (_hasRole[role][account]) {
revert PermissionsAlreadyGranted(account, role);
}
_setupRole(role, account);
}
/**
* @notice Revokes role from an account.
* @dev Caller must have admin role for the `role`.
* Emits {RoleRevoked Event}.
*
* @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE")
* @param account Address of the account from which the role is being revoked.
*/
function revokeRole(bytes32 role, address account) public virtual override {
_checkRole(_getRoleAdmin[role], msg.sender);
_revokeRole(role, account);
}
/**
* @notice Revokes role from the account.
* @dev Caller must have the `role`, with caller being the same as `account`.
* Emits {RoleRevoked Event}.
*
* @param role keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE")
* @param account Address of the account from which the role is being revoked.
*/
function renounceRole(bytes32 role, address account) public virtual override {
if (msg.sender != account) {
revert PermissionsInvalidPermission(msg.sender, account);
}
_revokeRole(role, account);
}
/// @dev Sets `adminRole` as `role`'s admin role.
function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual {
bytes32 previousAdminRole = _getRoleAdmin[role];
_getRoleAdmin[role] = adminRole;
emit RoleAdminChanged(role, previousAdminRole, adminRole);
}
/// @dev Sets up `role` for `account`
function _setupRole(bytes32 role, address account) internal virtual {
_hasRole[role][account] = true;
emit RoleGranted(role, account, msg.sender);
}
/// @dev Revokes `role` from `account`
function _revokeRole(bytes32 role, address account) internal virtual {
_checkRole(role, account);
delete _hasRole[role][account];
emit RoleRevoked(role, account, msg.sender);
}
/// @dev Checks `role` for `account`. Reverts with a message including the required role.
function _checkRole(bytes32 role, address account) internal view virtual {
if (!_hasRole[role][account]) {
revert PermissionsUnauthorizedAccount(account, role);
}
}
/// @dev Checks `role` for `account`. Reverts with a message including the required role.
function _checkRoleWithSwitch(bytes32 role, address account) internal view virtual {
if (!hasRoleWithSwitch(role, account)) {
revert PermissionsUnauthorizedAccount(account, role);
}
}
}
// File: @thirdweb-dev/contracts/extension/interface/IContractMetadata.sol
pragma solidity ^0.8.0;
/// @author thirdweb
/**
* Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI
* for you contract.
*
* Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea.
*/
interface IContractMetadata {
/// @dev Returns the metadata URI of the contract.
function contractURI() external view returns (string memory);
/**
* @dev Sets contract URI for the storefront-level metadata of the contract.
* Only module admin can call this function.
*/
function setContractURI(string calldata _uri) external;
/// @dev Emitted when the contract URI is updated.
event ContractURIUpdated(string prevURI, string newURI);
}
// File: @thirdweb-dev/contracts/extension/ContractMetadata.sol
pragma solidity ^0.8.0;
/// @author thirdweb
/**
* @title Contract Metadata
* @notice Thirdweb's `ContractMetadata` is a contract extension for any base contracts. It lets you set a metadata URI
* for you contract.
* Additionally, `ContractMetadata` is necessary for NFT contracts that want royalties to get distributed on OpenSea.
*/
abstract contract ContractMetadata is IContractMetadata {
/// @dev The sender is not authorized to perform the action
error ContractMetadataUnauthorized();
/// @notice Returns the contract metadata URI.
string public override contractURI;
/**
* @notice Lets a contract admin set the URI for contract-level metadata.
* @dev Caller should be authorized to setup contractURI, e.g. contract admin.
* See {_canSetContractURI}.
* Emits {ContractURIUpdated Event}.
*
* @param _uri keccak256 hash of the role. e.g. keccak256("TRANSFER_ROLE")
*/
function setContractURI(string memory _uri) external override {
if (!_canSetContractURI()) {
revert ContractMetadataUnauthorized();
}
_setupContractURI(_uri);
}
/// @dev Lets a contract admin set the URI for contract-level metadata.
function _setupContractURI(string memory _uri) internal {
string memory prevURI = contractURI;
contractURI = _uri;
emit ContractURIUpdated(prevURI, _uri);
}
/// @dev Returns whether contract metadata can be set in the given execution context.
function _canSetContractURI() 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/PrimapePrediction.sol
pragma solidity ^0.8.26;
/**
* @title PrimapePrediction
* @dev A multi-outcome, pari-mutuel style prediction market using the chain's native token for betting.
* - Supports multiple outcomes per market.
* - Allows early resolution by the owner.
* - Includes a platform fee adjustable by the owner.
* - Integrates ContractMetadata for contract-level metadata management.
* - Integrates Permissions (with owner as default admin) for future role-based expansions.
*/
contract PrimapePrediction is Ownable, ReentrancyGuard, Permissions, ContractMetadata {
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: marketId => array of outcome strings
mapping(uint256 => string[]) public marketOptions;
// Total shares per option: marketId => optionIndex => total 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.
uint256 public feeBps = 100; // Default 1% fee
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);
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
}
/**
* @dev Required override for Ownable extension.
* @return True if caller is contract owner.
*/
function _canSetOwner() internal view virtual override returns (bool) {
return msg.sender == owner();
}
/**
* @dev Required override for ContractMetadata extension to control who can set contract URI.
* @return True if caller is owner.
*/
function _canSetContractURI() internal view virtual override returns (bool) {
return msg.sender == owner();
}
/**
* @notice Create a new prediction market with multiple outcomes.
* @param _question The market question/prompt.
* @param _options The array of outcome strings (at least two).
* @param _duration Duration in seconds the market is active for.
* @return marketId The 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;
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 the 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");
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 at any time (early resolution allowed).
* @param _marketId The market ID.
* @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");
market.winningOptionIndex = _winningOptionIndex;
market.resolved = true;
emit MarketResolved(_marketId, _winningOptionIndex);
}
/**
* @notice Claim winnings after a market is resolved.
* @param _marketId The market ID.
*/
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];
}
}
uint256 rewardRatio = 0;
if (winningShares > 0) {
rewardRatio = (losingShares * 1e18) / winningShares;
}
uint256 winnings = userShares + (userShares * rewardRatio) / 1e18;
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 market ID.
* @param _users Array of 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 market ID.
* @return question The market's question.
* @return endTime The market's end time.
* @return resolved Whether the market is resolved.
* @return winningOptionIndex The winning option index 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 market ID.
* @return 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 market ID.
* @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 BPS. (e.g., 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 Address to receive withdrawn fees.
* @param _amount 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);
}
}