APE Price: $0.58 (+4.11%)

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

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

Context size (optional):