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