// SPDX-License-Identifier: MIT pragma solidity >=0.8.0 <0.9.0; contract OptimisticOracle { //////////////// /// Enums ////// //////////////// enum State { Invalid, Asserted, Proposed, Disputed, Settled, Expired } ///////////////// /// Errors ////// ///////////////// error AssertionNotFound(); error AssertionProposed(); error InvalidValue(); error InvalidTime(); error ProposalDisputed(); error NotProposedAssertion(); error AlreadyClaimed(); error AlreadySettled(); error AwaitingDecider(); error NotDisputedAssertion(); error OnlyDecider(); error OnlyOwner(); error TransferFailed(); ////////////////////// /// State Variables // ////////////////////// struct EventAssertion { address asserter; address proposer; address disputer; bool proposedOutcome; bool resolvedOutcome; uint256 reward; uint256 bond; uint256 startTime; uint256 endTime; bool claimed; address winner; string description; } uint256 public constant MINIMUM_ASSERTION_WINDOW = 3 minutes; uint256 public constant DISPUTE_WINDOW = 3 minutes; address public decider; address public owner; uint256 public nextAssertionId = 1; mapping(uint256 => EventAssertion) public assertions; //////////////// /// Events ///// //////////////// event EventAsserted(uint256 assertionId, address asserter, string description, uint256 reward); event OutcomeProposed(uint256 assertionId, address proposer, bool outcome); event OutcomeDisputed(uint256 assertionId, address disputer); event AssertionSettled(uint256 assertionId, bool outcome, address winner); event DeciderUpdated(address oldDecider, address newDecider); event RewardClaimed(uint256 assertionId, address winner, uint256 amount); event RefundClaimed(uint256 assertionId, address asserter, uint256 amount); /////////////////// /// Modifiers ///// /////////////////// /** * @notice Modifier to restrict function access to the designated decider * @dev Ensures only the decider can settle disputed assertions */ modifier onlyDecider() { if (msg.sender != decider) revert OnlyDecider(); _; } /** * @notice Modifier to restrict function access to the contract owner * @dev Ensures only the owner can update critical contract parameters */ modifier onlyOwner() { if (msg.sender != owner) revert OnlyOwner(); _; } /////////////////// /// Constructor /// /////////////////// constructor(address _decider) { decider = _decider; owner = msg.sender; } /////////////////// /// Functions ///// /////////////////// /** * @notice Updates the decider address (only contract owner) * @dev Changes the address authorized to settle disputed assertions. * Emits DeciderUpdated event with old and new addresses. * @param _decider The new address that will act as decider for disputed assertions */ function setDecider(address _decider) external onlyOwner { address oldDecider = address(decider); decider = _decider; emit DeciderUpdated(oldDecider, _decider); } /** * @notice Returns the complete assertion details for a given assertion ID * @dev Provides access to all fields of the EventAssertion struct * @param assertionId The unique identifier of the assertion to retrieve * @return The complete EventAssertion struct containing all assertion data */ function getAssertion(uint256 assertionId) external view returns (EventAssertion memory) { return assertions[assertionId]; } /** * @notice Creates a new assertion about an event with a true/false outcome * @dev Requires ETH payment as reward for correct proposers. Bond requirement is 2x the reward. * Sets default timestamps if not provided. Validates timing requirements. * @param description Human-readable description of the event (e.g. "Did X happen by time Y?") * @param startTime When proposals can begin (0 for current block timestamp) * @param endTime When the assertion expires (0 for startTime + minimum window) * @return The unique assertion ID for the newly created assertion */ function assertEvent( string memory description, uint256 startTime, uint256 endTime ) external payable returns (uint256) { uint256 assertionId = nextAssertionId; nextAssertionId++; if (msg.value == 0) revert InvalidValue(); if (startTime == 0) startTime = block.timestamp; if (endTime == 0) endTime = block.timestamp + MINIMUM_ASSERTION_WINDOW; if (startTime < block.timestamp) revert InvalidTime(); if (endTime < startTime + MINIMUM_ASSERTION_WINDOW) revert InvalidTime(); assertions[assertionId] = EventAssertion({ asserter: msg.sender, proposer: address(0), disputer: address(0), proposedOutcome: false, resolvedOutcome: false, reward: msg.value, bond: msg.value * 2, startTime: startTime, endTime: endTime, claimed: false, winner: address(0), description: description }); emit EventAsserted(assertionId, msg.sender, description, msg.value); return assertionId; } /** * @notice Proposes the outcome (true or false) for an asserted event * @dev Requires bonding ETH equal to 2x the original reward. Sets dispute window deadline. * Can only be called once per assertion and within the assertion time window. * @param assertionId The unique identifier of the assertion to propose an outcome for * @param outcome The proposed boolean outcome (true or false) for the event */ function proposeOutcome(uint256 assertionId, bool outcome) external payable { if (assertionId >= nextAssertionId) revert AssertionNotFound(); EventAssertion storage eventAssertion = assertions[assertionId]; if (eventAssertion.proposer != address(0)) revert AssertionProposed(); uint256 currentTime = block.timestamp; if (currentTime < eventAssertion.startTime || currentTime > eventAssertion.endTime) revert InvalidTime(); if (msg.value != eventAssertion.bond) revert InvalidValue(); eventAssertion.proposer = msg.sender; eventAssertion.proposedOutcome = outcome; eventAssertion.endTime = currentTime + DISPUTE_WINDOW; emit OutcomeProposed(assertionId, msg.sender, outcome); } /** * @notice Disputes a proposed outcome by bonding ETH * @dev Requires bonding ETH equal to the bond amount. Can only dispute once per assertion * and must be within the dispute window after proposal. * @param assertionId The unique identifier of the assertion to dispute */ function disputeOutcome(uint256 assertionId) external payable { EventAssertion storage eventAssertion = assertions[assertionId]; if (eventAssertion.asserter == address(0)) revert AssertionNotFound(); if (eventAssertion.proposer == address(0)) revert NotProposedAssertion(); if (eventAssertion.disputer != address(0)) revert ProposalDisputed(); if (block.timestamp > eventAssertion.endTime) revert InvalidTime(); if (msg.value != eventAssertion.bond) revert InvalidValue(); eventAssertion.disputer = msg.sender; emit OutcomeDisputed(assertionId, msg.sender); } /** * @notice Claims reward for undisputed assertions after dispute window expires * @dev Anyone can trigger this function. Transfers reward + bond to the proposer. * Can only be called after dispute window has passed without disputes. * @param assertionId The unique identifier of the assertion to claim rewards for */ function claimUndisputedReward(uint256 assertionId) external { EventAssertion storage assertion = assertions[assertionId]; if (assertion.proposer == address(0)) revert NotProposedAssertion(); if (assertion.disputer != address(0)) revert ProposalDisputed(); if (block.timestamp <= assertion.endTime) revert InvalidTime(); if (assertion.claimed) revert AlreadyClaimed(); assertion.claimed = true; assertion.resolvedOutcome = assertion.proposedOutcome; assertion.winner = assertion.proposer; uint256 totalValue = assertion.reward + assertion.bond; (bool success,) = payable(assertion.proposer).call{value: totalValue}(""); if (!success) revert TransferFailed(); emit RewardClaimed(assertionId, assertion.proposer, totalValue); } /** * @notice Claims reward for disputed assertions after decider settlement * @dev Anyone can trigger this function. Pays decider fee and transfers remaining rewards to winner. * Can only be called after decider has settled the dispute. * @param assertionId The unique identifier of the disputed assertion to claim rewards for */ function claimDisputedReward(uint256 assertionId) external { EventAssertion storage assertion = assertions[assertionId]; if (assertion.proposer == address(0)) revert NotProposedAssertion(); if (assertion.disputer == address(0)) revert NotDisputedAssertion(); if (assertion.winner == address(0)) revert AwaitingDecider(); if (assertion.claimed) revert AlreadyClaimed(); assertion.claimed = true; (bool successDecider,) = payable(decider).call{value: assertion.bond}(""); if (!successDecider) revert TransferFailed(); uint256 totalValue = assertion.reward + assertion.bond; (bool success,) = payable(assertion.winner).call{value: totalValue}(""); if (!success) revert TransferFailed(); emit RewardClaimed(assertionId, assertion.winner, totalValue); } /** * @notice Claims refund for assertions that receive no proposals before deadline * @dev Anyone can trigger this function. Returns the original reward to the asserter. * Can only be called after assertion deadline has passed without any proposals. * @param assertionId The unique identifier of the expired assertion to claim refund for */ function claimRefund(uint256 assertionId) external { EventAssertion storage assertion = assertions[assertionId]; if (assertion.asserter == address(0)) revert AssertionNotFound(); if (assertion.proposer != address(0)) revert AssertionProposed(); if (block.timestamp <= assertion.endTime) revert InvalidTime(); if (assertion.claimed) revert AlreadyClaimed(); assertion.claimed = true; uint256 totalValue = assertion.reward; (bool success,) = payable(assertion.asserter).call{value: totalValue}(""); if (!success) revert TransferFailed(); emit RefundClaimed(assertionId, assertion.asserter, totalValue); } /** * @notice Resolves disputed assertions by determining the correct outcome (only decider) * @dev Sets the resolved outcome and determines winner based on proposal accuracy. * @param assertionId The unique identifier of the disputed assertion to settle * @param resolvedOutcome The decider's determination of the true outcome */ function settleAssertion(uint256 assertionId, bool resolvedOutcome) external onlyDecider { EventAssertion storage assertion = assertions[assertionId]; if (assertion.proposer == address(0)) revert NotProposedAssertion(); if (assertion.disputer == address(0)) revert NotDisputedAssertion(); if (assertion.winner != address(0)) revert AlreadySettled(); assertion.resolvedOutcome = resolvedOutcome; assertion.winner = assertion.proposedOutcome == resolvedOutcome ? assertion.proposer : assertion.disputer; emit AssertionSettled(assertionId, resolvedOutcome, assertion.winner); } /** * @notice Returns the current state of an assertion based on its lifecycle stage * @dev Evaluates assertion progress through states: Invalid, Asserted, Proposed, Disputed, Settled, Expired * @param assertionId The unique identifier of the assertion to check state for * @return The current State enum value representing the assertion's status */ function getState(uint256 assertionId) external view returns (State) { EventAssertion memory assertion = assertions[assertionId]; if (assertion.asserter == address(0)) return State.Invalid; if (assertion.winner != address(0)) return State.Settled; if (assertion.disputer != address(0)) return State.Disputed; if (assertion.proposer != address(0)) { if (block.timestamp >= assertion.endTime) return State.Settled; return State.Proposed; } if (block.timestamp >= assertion.endTime) return State.Expired; return State.Asserted; } /** * @notice Returns the final resolved outcome of a settled assertion * @dev For undisputed assertions, returns the proposed outcome after dispute window. * For disputed assertions, returns the decider's resolved outcome. * @param assertionId The unique identifier of the assertion to get resolution for * @return The final boolean outcome of the assertion */ function getResolution(uint256 assertionId) external view returns (bool) { EventAssertion memory assertion = assertions[assertionId]; if (assertion.asserter == address(0)) revert AssertionNotFound(); if (assertion.proposer == address(0)) revert NotProposedAssertion(); if (assertion.winner != address(0)) return assertion.resolvedOutcome; if (assertion.disputer == address(0)) { if (block.timestamp <= assertion.endTime) revert InvalidTime(); return assertion.proposedOutcome; } revert AwaitingDecider(); } }