APE Price: $1.36 (+0.41%)

Contract Diff Checker

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);
    }
}

Please enter a contract address above to load the contract details and source code.

Context size (optional):