diff --git a/contracts/AdjudicationFramework.sol b/contracts/AdjudicationFramework.sol deleted file mode 100644 index 803bedd9..00000000 --- a/contracts/AdjudicationFramework.sol +++ /dev/null @@ -1,498 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -pragma solidity ^0.8.20; - -/* solhint-disable var-name-mixedcase */ -/* solhint-disable quotes */ -/* solhint-disable not-rely-on-time */ - -import {BalanceHolder} from "./lib/reality-eth/BalanceHolder.sol"; -import {IRealityETH} from "./lib/reality-eth/interfaces/IRealityETH.sol"; -import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; - -/* -This contract sits between a Reality.eth instance and an Arbitrator. -It manages a allowlist of arbitrators, and makes sure questions can be sent to an arbitrator on the allowlist. -When called on to arbitrate, it pays someone to send out the arbitration job to an arbitrator on the allowlist. -Arbitrators can be disputed on L1. -To Reality.eth it looks like a normal arbitrator, implementing the Arbitrator interface. -To the normal Arbitrator contracts that does its arbitration jobs, it looks like Reality.eth. -*/ - -contract AdjudicationFramework is BalanceHolder { - uint256 public constant ARB_DISPUTE_TIMEOUT = 86400; - uint256 public constant QUESTION_UNHANDLED_TIMEOUT = 86400; - - uint32 public constant REALITY_ETH_TIMEOUT = 86400; - uint32 public constant REALITY_ETH_BOND_ARBITRATOR_ADD = 10000; - uint32 public constant REALITY_ETH_BOND_ARBITRATOR_REMOVE = 10000; - uint32 public constant REALITY_ETH_BOND_ARBITRATOR_FREEZE = 20000; - - uint256 public templateIdAddArbitrator; - uint256 public templateIdRemoveArbitrator; - - event LogRequestArbitration( - bytes32 indexed question_id, - uint256 fee_paid, - address requester, - uint256 remaining - ); - - event LogNotifyOfArbitrationRequest( - bytes32 indexed question_id, - address indexed user - ); - - // AllowList of acceptable arbitrators - mapping(address => bool) public arbitrators; - - enum PropositionType { - NONE, - ADD_ARBITRATOR, - REMOVE_ARBITRATOR, - UPGRADE_BRIDGE - } - - // Reality.eth questions for propositions we may be asked to rule on - struct ArbitratorProposition { - PropositionType proposition_type; - address arbitrator; - bool isFrozen; - } - mapping(bytes32 => ArbitratorProposition) public propositions; - - // Keep a count of active propositions that freeze an arbitrator. - // When they're all cleared they can be unfrozen. - mapping(address => uint256) public countArbitratorFreezePropositions; - - IRealityETH public realityETH; - - // Arbitrator used for requesting a fork in the L1 chain in add/remove propositions - address public forkArbitrator; - - uint256 public dispute_fee; - - struct ArbitrationRequest { - address arbitrator; - address payer; - uint256 bounty; - bytes32 msg_hash; - uint256 finalize_ts; - uint256 last_action_ts; - } - - mapping(bytes32 => ArbitrationRequest) public question_arbitrations; - - /// @param _realityETH The reality.eth instance we adjudicate for - /// @param _dispute_fee The dispute fee we charge reality.eth users - /// @param _forkArbitrator The arbitrator contract that escalates to an L1 fork, used for our governance - /// @param _initialArbitrators Arbitrator contracts we initially support - constructor( - address _realityETH, - uint256 _dispute_fee, - address _forkArbitrator, - address[] memory _initialArbitrators - ) { - realityETH = IRealityETH(_realityETH); - dispute_fee = _dispute_fee; - forkArbitrator = _forkArbitrator; - - // Create reality.eth templates for our add and remove questions - // We'll identify ourselves in the template so we only need a single parameter for questions, the arbitrator in question. - // TODO: We may want to specify a document with the terms that guide this decision here, rather than just leaving it implicit. - - string - memory templatePrefixAdd = '{"title": "Should we add arbitrator %s to the framework '; - string - memory templatePrefixRemove = '{"title": "Should we remove arbitrator %s from the framework '; - string - memory templateSuffix = '?", "type": "bool", "category": "adjudication", "lang": "en"}'; - - string memory thisContractStr = Strings.toHexString(address(this)); - string memory addTemplate = string.concat( - templatePrefixAdd, - thisContractStr, - templateSuffix - ); - string memory removeTemplate = string.concat( - templatePrefixRemove, - thisContractStr, - templateSuffix - ); - - templateIdAddArbitrator = realityETH.createTemplate(addTemplate); - templateIdRemoveArbitrator = realityETH.createTemplate(removeTemplate); - - for (uint256 i = 0; i < _initialArbitrators.length; i++) { - arbitrators[_initialArbitrators[i]] = true; - } - } - - /// @notice Return the dispute fee for the specified question. 0 indicates that we won't arbitrate it. - /// @dev Uses a general default, but can be over-ridden on a question-by-question basis. - function getDisputeFee(bytes32) public view returns (uint256) { - // TODO: Should we have a governance process to change this? - return dispute_fee; - } - - /// @notice Request arbitration, freezing the question until we send submitAnswerByArbitrator - /// @dev Will trigger an error if the notification fails, eg because the question has already been finalized - /// @param question_id The question in question - /// @param max_previous If specified, reverts if a bond higher than this was submitted after you sent your transaction. - function requestArbitration( - bytes32 question_id, - uint256 max_previous - ) external payable returns (bool) { - uint256 arbitration_fee = getDisputeFee(question_id); - require( - arbitration_fee > 0, - "Question must have fee" // "The arbitrator must have set a non-zero fee for the question" - ); - require(msg.value >= arbitration_fee, "Insufficient fee"); - - realityETH.notifyOfArbitrationRequest( - question_id, - msg.sender, - max_previous - ); - emit LogRequestArbitration(question_id, msg.value, msg.sender, 0); - - // Queue the question for arbitration by a allowlisted arbitrator - // Anybody can take the question off the queue and submit it to a allowlisted arbitrator - // They will have to pay the arbitration fee upfront - // They can claim the bounty when they get an answer - // If the arbitrator is removed in the meantime, they'll lose the money they spent on arbitration - question_arbitrations[question_id].payer = msg.sender; - question_arbitrations[question_id].bounty = msg.value; - question_arbitrations[question_id].last_action_ts = block.timestamp; - - return true; - } - - // This function is normally in Reality.eth. - // We put it here so that we can be treated like Reality.eth from the pov of the arbitrator contract. - - /// @notice Notify the contract that the arbitrator has been paid for a question, freezing it pending their decision. - /// @dev The arbitrator contract is trusted to only call this if they've been paid, and tell us who paid them. - /// @param question_id The ID of the question - /// @param requester The account that requested arbitration - function notifyOfArbitrationRequest( - bytes32 question_id, - address requester, - uint256 - ) external { - require(arbitrators[msg.sender], "Arbitrator not allowlisted"); - require( - question_arbitrations[question_id].bounty > 0, - "Not in queue" // Question must be in the arbitration queue - ); - - // The only time you can pick up a question that's already being arbitrated is if it's been removed from the allowlist - if (question_arbitrations[question_id].arbitrator != address(0)) { - require( - !arbitrators[question_arbitrations[question_id].arbitrator], - "Question under arbitration" // Question already taken, and the arbitrator who took it is still active - ); - - // Clear any in-progress data from the arbitrator that has now been removed - question_arbitrations[question_id].msg_hash = 0x0; - question_arbitrations[question_id].finalize_ts = 0; - } - - question_arbitrations[question_id].payer = requester; - question_arbitrations[question_id].arbitrator = msg.sender; - - emit LogNotifyOfArbitrationRequest(question_id, requester); - } - - /// @notice Clear the arbitrator setting of an arbitrator that has been delisted - /// @param question_id The question in question - /// @dev Starts the clock ticking to allow us to cancelUnhandledArbitrationRequest - /// @dev Not otherwise needed, if another arbitrator shows up they can just take the job from the delisted arbitrator - function clearRequestFromRemovedArbitrator(bytes32 question_id) external { - address old_arbitrator = question_arbitrations[question_id].arbitrator; - require(old_arbitrator != address(0), "No arbitrator to remove"); - require( - !arbitrators[old_arbitrator], - "Arbitrator not removed" // Arbitrator must no longer be on the allowlist - ); - - question_arbitrations[question_id].arbitrator = address(0); - question_arbitrations[question_id].msg_hash = 0x0; - question_arbitrations[question_id].finalize_ts = 0; - - question_arbitrations[question_id].last_action_ts = block.timestamp; - } - - /// @notice Cancel the request for arbitration - /// @param question_id The question in question - /// @dev This is only done if nobody takes the request off the queue, probably because the fee is too low - function cancelUnhandledArbitrationRequest(bytes32 question_id) external { - uint256 last_action_ts = question_arbitrations[question_id] - .last_action_ts; - require(last_action_ts > 0, "Question not found"); - - require( - question_arbitrations[question_id].arbitrator == address(0), - "Already under arbitration" // Question already accepted by an arbitrator - ); - require( - block.timestamp - last_action_ts > QUESTION_UNHANDLED_TIMEOUT, - "Too soon to cancel" // You can only cancel questions that no arbitrator has accepted in a reasonable time - ); - - // Refund the arbitration bounty - balanceOf[question_arbitrations[question_id].payer] = - balanceOf[question_arbitrations[question_id].payer] + - question_arbitrations[question_id].bounty; - delete question_arbitrations[question_id]; - realityETH.cancelArbitration(question_id); - } - - // The arbitrator submits the answer to us, instead of to realityETH - // Instead of sending it to Reality.eth, we instead hold onto it for a challenge period in case someone disputes the arbitrator. - // TODO: We may need assignWinnerAndSubmitAnswerByArbitrator here instead - - /// @notice Submit the arbitrator's answer to a question. - /// @param question_id The question in question - /// @param answer The answer - /// @param answerer The answerer. If arbitration changed the answer, it should be the payer. If not, the old answerer. - /// @dev solc will complain about unsued params but they're used, just via msg.data - function submitAnswerByArbitrator( - bytes32 question_id, - bytes32 answer, - address answerer - ) public { - require( - question_arbitrations[question_id].arbitrator == msg.sender, - "Sender not the arbitrator" // An arbitrator can only submit their own arbitration result - ); - require( - question_arbitrations[question_id].bounty > 0, - "Question not in queue" - ); - - bytes32 data_hash = keccak256( - abi.encodePacked(question_id, answer, answerer) - ); - uint256 finalize_ts = block.timestamp + ARB_DISPUTE_TIMEOUT; - - question_arbitrations[question_id].msg_hash = data_hash; - question_arbitrations[question_id].finalize_ts = finalize_ts; - } - - /// @notice Resubmit the arbitrator's answer to a question once the challenge period for it has passed - /// @param question_id The question in question - /// @param answer The answer - /// @param answerer The answerer. If arbitration changed the answer, it should be the payer. If not, the old answerer. - function completeArbitration( - bytes32 question_id, - bytes32 answer, - address answerer - ) external { - address arbitrator = question_arbitrations[question_id].arbitrator; - - require(arbitrators[arbitrator], "Arbitrator must be allowlisted"); - require( - countArbitratorFreezePropositions[arbitrator] == 0, - "Arbitrator under dispute" - ); - - bytes32 data_hash = keccak256( - abi.encodePacked(question_id, answer, answerer) - ); - require( - question_arbitrations[question_id].msg_hash == data_hash, - "Resubmit previous parameters" - ); - - uint256 finalize_ts = question_arbitrations[question_id].finalize_ts; - require(finalize_ts > 0, "Submission must have been queued"); - require(finalize_ts < block.timestamp, "Challenge deadline not passed"); - - balanceOf[question_arbitrations[question_id].payer] = - balanceOf[question_arbitrations[question_id].payer] + - question_arbitrations[question_id].bounty; - - realityETH.submitAnswerByArbitrator(question_id, answer, answerer); - } - - // Governance (specifically adding and removing arbitrators from the allowlist) has two steps: - // 1) Create question - // 2) Complete operation (if proposition succeeded) or nothing if it failed - - // For time-sensitive operations, we also freeze any interested parties, so - // 1) Create question - // 2) Prove sufficient bond posted, freeze - // 3) Complete operation or Undo freeze - - function beginAddArbitratorToAllowList( - address arbitrator_to_add - ) external returns (bytes32) { - string memory question = Strings.toHexString(arbitrator_to_add); - bytes32 question_id = realityETH.askQuestionWithMinBond( - templateIdAddArbitrator, - question, - forkArbitrator, - REALITY_ETH_TIMEOUT, - uint32(block.timestamp), - 0, - REALITY_ETH_BOND_ARBITRATOR_ADD - ); - require( - propositions[question_id].proposition_type == PropositionType.NONE, - "Proposition already exists" - ); - propositions[question_id] = ArbitratorProposition( - PropositionType.ADD_ARBITRATOR, - arbitrator_to_add, - false - ); - return question_id; - } - - function beginRemoveArbitratorFromAllowList( - address arbitrator_to_remove - ) external returns (bytes32) { - string memory question = Strings.toHexString(arbitrator_to_remove); - bytes32 question_id = realityETH.askQuestionWithMinBond( - templateIdRemoveArbitrator, - question, - forkArbitrator, - REALITY_ETH_TIMEOUT, - uint32(block.timestamp), - 0, - REALITY_ETH_BOND_ARBITRATOR_REMOVE - ); - require( - propositions[question_id].proposition_type == PropositionType.NONE, - "Proposition already exists" - ); - propositions[question_id] = ArbitratorProposition( - PropositionType.REMOVE_ARBITRATOR, - arbitrator_to_remove, - false - ); - - return question_id; - } - - function executeAddArbitratorToAllowList(bytes32 question_id) external { - require( - propositions[question_id].proposition_type == - PropositionType.ADD_ARBITRATOR, - "Wrong Proposition type" - ); - address arbitrator = propositions[question_id].arbitrator; - require(!arbitrators[arbitrator], "Arbitrator already on allowlist"); - require( - realityETH.resultFor(question_id) == bytes32(uint256(1)), - "Question did not return yes" - ); - delete (propositions[question_id]); - - // NB They may still be in a frozen state because of some other proposition - arbitrators[arbitrator] = true; - } - - function executeRemoveArbitratorFromAllowList( - bytes32 question_id - ) external { - require( - propositions[question_id].proposition_type == - PropositionType.REMOVE_ARBITRATOR, - "Wrong Proposition type" - ); - - // NB This will run even if the arbitrator has already been removed by another proposition. - // This is needed so that the freeze can be cleared if the arbitrator is then reinstated. - - address arbitrator = propositions[question_id].arbitrator; - bytes32 realityEthResult = realityETH.resultFor(question_id); - require(realityEthResult == bytes32(uint256(1)), "Result was not 1"); - - if (propositions[question_id].isFrozen) { - countArbitratorFreezePropositions[arbitrator] = - countArbitratorFreezePropositions[arbitrator] - - 1; - } - delete (propositions[question_id]); - - arbitrators[arbitrator] = false; - } - - // When an arbitrator is listed for removal, they can be frozen given a sufficient bond - function freezeArbitrator( - bytes32 question_id, - bytes32[] memory history_hashes, - address[] memory addrs, - uint256[] memory bonds, - bytes32[] memory answers - ) public { - require( - propositions[question_id].proposition_type == - PropositionType.REMOVE_ARBITRATOR, - "Wrong Proposition type" - ); - address arbitrator = propositions[question_id].arbitrator; - - require( - arbitrators[arbitrator], - "Arbitrator not allowlisted" // Not allowlisted in the first place - ); - require( - !propositions[question_id].isFrozen, - "Arbitrator already frozen" - ); - - // Require a bond of at least the specified level - // This is only relevant if REALITY_ETH_BOND_ARBITRATOR_FREEZE is higher than REALITY_ETH_BOND_ARBITRATOR_REMOVE - - bytes32 answer; - uint256 bond; - // Normally you call this right after posting your answer so your final answer will be the current answer - // If someone has since submitted a different answer, you need to pass in the history from now until yours - if (history_hashes.length == 0) { - answer = realityETH.getBestAnswer(question_id); - bond = realityETH.getBond(question_id); - } else { - (answer, bond) = realityETH - .getEarliestAnswerFromSuppliedHistoryOrRevert( - question_id, - history_hashes, - addrs, - bonds, - answers - ); - } - - require(answer == bytes32(uint256(1)), "Supplied answer is not yes"); - require( - bond >= REALITY_ETH_BOND_ARBITRATOR_FREEZE, - "Bond too low to freeze" - ); - - // TODO: Ideally we would check the bond is for the "remove" answer. - // #92 - - propositions[question_id].isFrozen = true; - countArbitratorFreezePropositions[arbitrator] = - countArbitratorFreezePropositions[arbitrator] + - 1; - } - - function clearFailedProposition(bytes32 question_id) public { - address arbitrator = propositions[question_id].arbitrator; - require(arbitrator != address(0), "Proposition not found"); - if (propositions[question_id].isFrozen) { - countArbitratorFreezePropositions[arbitrator] = - countArbitratorFreezePropositions[arbitrator] - - 1; - } - delete (propositions[question_id]); - } - - function realitio() external view returns (address) { - return address(realityETH); - } -} diff --git a/contracts/AdjudicationFramework/MinimalAdjudicationFramework.sol b/contracts/AdjudicationFramework/MinimalAdjudicationFramework.sol new file mode 100644 index 00000000..82fe06d5 --- /dev/null +++ b/contracts/AdjudicationFramework/MinimalAdjudicationFramework.sol @@ -0,0 +1,303 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +/* solhint-disable quotes */ +/* solhint-disable not-rely-on-time */ + +import {IRealityETH} from "./../lib/reality-eth/interfaces/IRealityETH.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +/* +Minimal Adjudication framework every framework should implement. +Contains an enumerableSet of Arbitrators. +Arbitrators can be removed or added by providing a realityETH question with forking as a final arbitration. +Also, arbitrators who are challenged by a removal question, can be temporarily frozen, if a sufficient bond is provided. +*/ + +contract MinimalAdjudicationFramework { + using EnumerableSet for EnumerableSet.AddressSet; + /// @dev Error thrown with illegal modification of arbitrators + error NoArbitratorsToModify(); + /// @dev Error thrown when a proposition already exists + error PropositionAlreadyExists(); + /// @dev Error thrown when a proposition is not found + error PropositionNotFound(); + /// @dev Error thrown when a proposition is not found + error ArbitratorNotInAllowList(); + /// @dev Error thrown when an arbitrator is already frozen + error ArbitratorAlreadyFrozen(); + /// @dev Error thrown when received messages from realityEth is not yes + error AnswerNotYes(); + /// @dev Error thrown when received messages from realityEth is yes, but expected to be no + error PropositionNotFailed(); + /// @dev Error thrown when bond is too low to freeze an arbitrator + error BondTooLowToFreeze(); + /// @dev Error thrown when proposition is not accepted + error PropositionNotAccepted(); + + // Question delimiter for arbitrator modification questions for reality.eth + string internal constant _QUESTION_DELIM = "\u241f"; + + EnumerableSet.AddressSet internal _arbitrators; + /// @dev Error thrown when non-allowlisted actor tries to call a function + error OnlyAllowlistedActor(); + /// @dev Error thrown when multiple modifications are requested at once + error NoMultipleModificationsAtOnce(); + + // Iterable list contains list of allowlisted arbitrators + uint256 public constant ARB_DISPUTE_TIMEOUT = 86400; + uint256 public constant QUESTION_UNHANDLED_TIMEOUT = 86400; + + uint32 public constant REALITY_ETH_TIMEOUT = 86400; + uint32 public constant REALITY_ETH_BOND_ARBITRATOR_REMOVE = 10000; + uint32 public constant REALITY_ETH_BOND_ARBITRATOR_FREEZE = 20000; + + // Template used to remove arbitrators, in case they are misbehaving + uint256 public templateIdRemoveArbitrator; + uint256 public templateIdAddArbitrator; + uint256 public templateIdReplaceArbitrator; + + bool public allowReplacementModification; + + // Contract used for requesting a fork in the L1 chain in remove propositions + address public forkArbitrator; + + // Reality.eth questions for propositions we may be asked to rule on + struct ArbitratorProposition { + address arbitratorToRemove; + address arbitratorToAdd; + bool isFrozen; + } + mapping(bytes32 => ArbitratorProposition) public propositions; + + // Keep a count of active propositions that freeze an arbitrator. + // When they're all cleared they can be unfrozen. + mapping(address => uint256) public countArbitratorFreezePropositions; + + IRealityETH public realityETH; + + modifier onlyArbitrator() { + if (!_arbitrators.contains(msg.sender)) { + revert OnlyAllowlistedActor(); + } + _; + } + + /// @param _realityETH The reality.eth instance we adjudicate for + /// @param _forkArbitrator The arbitrator contract that escalates to an L1 fork, used for our governance + /// @param _initialArbitrators Arbitrator contracts we initially support + /// @param _allowReplacementModification Whether to allow multiple modifications at once + constructor( + address _realityETH, + address _forkArbitrator, + address[] memory _initialArbitrators, + bool _allowReplacementModification + ) { + allowReplacementModification = _allowReplacementModification; + realityETH = IRealityETH(_realityETH); + forkArbitrator = _forkArbitrator; + // Create reality.eth templates for our add questions + // We'll identify ourselves in the template so we only need a single parameter for questions, the arbitrator in question. + // TODO: We may want to specify a document with the terms that guide this decision here, rather than just leaving it implicit. + string + memory templatePrefixReplace = '{"title": "Should we replace the arbitrator %s by the new arbitrator %s in the framework '; + string + memory templatePrefixAdd = '{"title": "Should we add the arbitrator %s to the framework '; + string + memory templatePrefixRemove = '{"title": "Should we remove the arbitrator %s from the framework '; + string + memory templateSuffix = '?", "type": "bool", "category": "adjudication", "lang": "en"}'; + string memory thisContractStr = Strings.toHexString(address(this)); + string memory removeTemplate = string.concat( + templatePrefixRemove, + thisContractStr, + templateSuffix + ); + string memory addTemplate = string.concat( + templatePrefixAdd, + thisContractStr, + templateSuffix + ); + string memory replaceTemplate = string.concat( + templatePrefixReplace, + thisContractStr, + templateSuffix + ); + templateIdRemoveArbitrator = realityETH.createTemplate(removeTemplate); + templateIdAddArbitrator = realityETH.createTemplate(addTemplate); + templateIdReplaceArbitrator = realityETH.createTemplate( + replaceTemplate + ); + + // Allowlist the initial arbitrators + for (uint256 i = 0; i < _initialArbitrators.length; i++) { + _arbitrators.add(_initialArbitrators[i]); + } + } + + function requestModificationOfArbitrators( + address arbitratorToRemove, + address arbitratorToAdd + ) external returns (bytes32) { + string memory question; + uint256 templateId; + if (arbitratorToRemove == address(0) && arbitratorToAdd == address(0)) { + revert NoArbitratorsToModify(); + } else if (arbitratorToRemove == address(0)) { + question = Strings.toHexString(arbitratorToAdd); + templateId = templateIdAddArbitrator; + } else if (arbitratorToAdd == address(0)) { + question = Strings.toHexString(arbitratorToRemove); + templateId = templateIdRemoveArbitrator; + } else { + if (!allowReplacementModification) { + revert NoMultipleModificationsAtOnce(); + } + question = string.concat( + Strings.toHexString(arbitratorToRemove), + _QUESTION_DELIM, + Strings.toHexString(arbitratorToAdd) + ); + templateId = templateIdReplaceArbitrator; + } + bytes32 questionId = realityETH.askQuestionWithMinBond( + templateIdRemoveArbitrator, + question, + forkArbitrator, + REALITY_ETH_TIMEOUT, + uint32(block.timestamp), + 0, + REALITY_ETH_BOND_ARBITRATOR_REMOVE + ); + if ( + propositions[questionId].arbitratorToAdd != address(0) || + propositions[questionId].arbitratorToRemove != address(0) + ) { + revert PropositionAlreadyExists(); + } + propositions[questionId] = ArbitratorProposition( + arbitratorToRemove, + arbitratorToAdd, + false + ); + return questionId; + } + + function executeModificationArbitratorFromAllowList( + bytes32 questionId + ) external { + // NB This will run even if the arbitrator has already been removed by another proposition. + // This is needed so that the freeze can be cleared if the arbitrator is then reinstated. + + address arbitratorToRemove = propositions[questionId] + .arbitratorToRemove; + address arbitratorToAdd = propositions[questionId].arbitratorToAdd; + bytes32 realityEthResult = realityETH.resultFor(questionId); + if (realityEthResult != bytes32(uint256(1))) { + revert PropositionNotAccepted(); + } + if (arbitratorToRemove != address(0)) { + _arbitrators.remove(arbitratorToRemove); + if (propositions[questionId].isFrozen) { + countArbitratorFreezePropositions[arbitratorToRemove] -= 1; + } + } + if (arbitratorToAdd != address(0)) { + _arbitrators.add(arbitratorToAdd); + } + delete (propositions[questionId]); + } + + // When an arbitrator is listed for removal, they can be frozen given a sufficient bond + function freezeArbitrator( + bytes32 questionId, + bytes32[] memory historyHashes, + address[] memory addrs, + uint256[] memory bonds, + bytes32[] memory answers + ) public { + address arbitrator = propositions[questionId].arbitratorToRemove; + + if (arbitrator == address(0)) { + revert PropositionNotFound(); + } + if (!_arbitrators.contains(arbitrator)) { + revert ArbitratorNotInAllowList(); + } + if (propositions[questionId].isFrozen) { + revert ArbitratorAlreadyFrozen(); + } + + // Require a bond of at least the specified level + // This is only relevant if REALITY_ETH_BOND_ARBITRATOR_FREEZE is higher than REALITY_ETH_BOND_ARBITRATOR_REMOVE + + bytes32 answer; + uint256 bond; + // Normally you call this right after posting your answer so your final answer will be the current answer + // If someone has since submitted a different answer, you need to pass in the history from now until yours + if (historyHashes.length == 0) { + answer = realityETH.getBestAnswer(questionId); + bond = realityETH.getBond(questionId); + } else { + (answer, bond) = realityETH + .getEarliestAnswerFromSuppliedHistoryOrRevert( + questionId, + historyHashes, + addrs, + bonds, + answers + ); + } + + if (answer != bytes32(uint256(1))) { + revert AnswerNotYes(); + } + if (bond < REALITY_ETH_BOND_ARBITRATOR_FREEZE) { + revert BondTooLowToFreeze(); + } + + // TODO: Ideally we would check the bond is for the "remove" answer. + // #92 + + propositions[questionId].isFrozen = true; + countArbitratorFreezePropositions[arbitrator] = + countArbitratorFreezePropositions[arbitrator] + + 1; + } + + function clearFailedProposition(bytes32 questionId) public { + address arbitrator = propositions[questionId].arbitratorToRemove; + if (arbitrator == address(0)) { + revert PropositionNotFound(); + } + + bytes32 realityEthResult = realityETH.resultFor(questionId); + if (realityEthResult == bytes32(uint256(1))) { + revert PropositionNotFailed(); + } + if (propositions[questionId].isFrozen) { + countArbitratorFreezePropositions[arbitrator] = + countArbitratorFreezePropositions[arbitrator] - + 1; + } + delete (propositions[questionId]); + } + + // Getter functions only below here + + function realitio() external view returns (address) { + return address(realityETH); + } + + function isArbitrator(address arbitrator) external view returns (bool) { + return _arbitrators.contains(arbitrator); + } + + function isArbitratorPropositionFrozen( + bytes32 questionId + ) external view returns (bool) { + return propositions[questionId].isFrozen; + } +} diff --git a/contracts/AdjudicationFramework/Pull/AdjudicationFrameworkRequests.sol b/contracts/AdjudicationFramework/Pull/AdjudicationFrameworkRequests.sol new file mode 100644 index 00000000..87886485 --- /dev/null +++ b/contracts/AdjudicationFramework/Pull/AdjudicationFrameworkRequests.sol @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +/* solhint-disable var-name-mixedcase */ +/* solhint-disable quotes */ +/* solhint-disable not-rely-on-time */ + +import {BalanceHolder} from "./../../lib/reality-eth/BalanceHolder.sol"; +import {MinimalAdjudicationFramework} from "../MinimalAdjudicationFramework.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +/* +This contract sits between a Reality.eth instance and an Arbitrator. +It manages a allowlist of arbitrators, and makes sure questions can be sent to an arbitrator on the allowlist. +When called on to arbitrate, it pays someone to send out the arbitration job to an arbitrator on the allowlist. +Arbitrators can be disputed on L1. +To Reality.eth it looks like a normal arbitrator, implementing the Arbitrator interface. +To the normal Arbitrator contracts that does its arbitration jobs, it looks like Reality.eth. +*/ + +contract AdjudicationFrameworkRequests is + MinimalAdjudicationFramework, + BalanceHolder +{ + using EnumerableSet for EnumerableSet.AddressSet; + /// @dev Error thrown when challenge deadline has not passed + error ChallengeDeadlineNotPassed(); + /// @dev Error thrown when submission must have been queued + error SubmissionMustHaveBeenQueued(); + /// @dev Error thrown when resubmission of previous parameters + error ResubmissionOfPReviousParameters(); + /// @dev Error thrown when arbitrator must be allowlisted + error ArbitratorMustBeAllowlisted(); + /// @dev Error thrown when arbitrator under dispute + error ArbitratorUnderDispute(); + /// @dev Error thrown when arbitrator address is zero + error ArbitratorAddressZero(); + /// @dev Error thrown when arbitrator not removed + error ArbitratorNotRemoved(); + /// @dev Error thrown when arbitrator not sender + error ArbitratorNotSender(); + /// @dev Error thrown when question already under arbitration + error QuestionAlreadyUnderArbitration(); + /// @dev Error thrown when question under arbitration + error QuestionUnderArbitration(); + /// @dev Error thrown when question not in queue + error QuestionNotFound(); + /// @dev Error thrown when question must have a fee + error QuestionMustHaveAFee(); + /// @dev Error thrown when insufficient fee + error InsufficientFee(); + /// @dev Error thrown when question not in queue + error QuestionNotInQueue(); + /// @dev Error thrown when too soon to cancel + error TooSoonToCancel(); + + event LogRequestArbitration( + bytes32 indexed questionId, + uint256 feePaid, + address requester, + uint256 remaining + ); + + event LogNotifyOfArbitrationRequest( + bytes32 indexed questionId, + address indexed user + ); + + uint256 public dispute_fee; + + struct ArbitrationRequest { + address arbitrator; + address payer; + uint256 bounty; + bytes32 msg_hash; + uint256 finalize_ts; + uint256 last_action_ts; + } + + mapping(bytes32 => ArbitrationRequest) public questionArbitrations; + + /// @param _realityETH The reality.eth instance we adjudicate for + /// @param _disputeFee The dispute fee we charge reality.eth users + /// @param _forkArbitrator The arbitrator contract that escalates to an L1 fork, used for our governance + /// @param _initialArbitrators Arbitrator contracts we initially support + constructor( + address _realityETH, + uint256 _disputeFee, + address _forkArbitrator, + address[] memory _initialArbitrators, + bool _allowReplacementModification + ) + MinimalAdjudicationFramework( + _realityETH, + _forkArbitrator, + _initialArbitrators, + _allowReplacementModification + ) + { + dispute_fee = _disputeFee; + } + + /// @notice Return the dispute fee for the specified question. 0 indicates that we won't arbitrate it. + /// @dev Uses a general default, but can be over-ridden on a question-by-question basis. + function getDisputeFee(bytes32) public view returns (uint256) { + // TODO: Should we have a governance process to change this? + return dispute_fee; + } + + /// @notice Request arbitration, freezing the question until we send submitAnswerByArbitrator + /// @dev Will trigger an error if the notification fails, eg because the question has already been finalized + /// @param questionId The question in question + /// @param max_previous If specified, reverts if a bond higher than this was submitted after you sent your transaction. + function requestArbitration( + bytes32 questionId, + uint256 max_previous + ) external payable returns (bool) { + uint256 arbitration_fee = getDisputeFee(questionId); + if (arbitration_fee == 0) { + revert QuestionMustHaveAFee(); + } + if (msg.value < arbitration_fee) { + revert InsufficientFee(); + } + + realityETH.notifyOfArbitrationRequest( + questionId, + msg.sender, + max_previous + ); + emit LogRequestArbitration(questionId, msg.value, msg.sender, 0); + + // Queue the question for arbitration by a allowlisted arbitrator + // Anybody can take the question off the queue and submit it to a allowlisted arbitrator + // They will have to pay the arbitration fee upfront + // They can claim the bounty when they get an answer + // If the arbitrator is removed in the meantime, they'll lose the money they spent on arbitration + questionArbitrations[questionId].payer = msg.sender; + questionArbitrations[questionId].bounty = msg.value; + questionArbitrations[questionId].last_action_ts = block.timestamp; + + return true; + } + + // This function is normally in Reality.eth. + // We put it here so that we can be treated like Reality.eth from the pov of the arbitrator contract. + + /// @notice Notify the contract that the arbitrator has been paid for a question, freezing it pending their decision. + /// @dev The arbitrator contract is trusted to only call this if they've been paid, and tell us who paid them. + /// @param questionId The ID of the question + /// @param requester The account that requested arbitration + function notifyOfArbitrationRequest( + bytes32 questionId, + address requester, + uint256 + ) external onlyArbitrator { + if (questionArbitrations[questionId].bounty == 0) { + revert QuestionNotInQueue(); + } + + // The only time you can pick up a question that's already being arbitrated is if it's been removed from the allowlist + if (questionArbitrations[questionId].arbitrator != address(0)) { + if ( + _arbitrators.contains( + questionArbitrations[questionId].arbitrator + ) + ) { + revert QuestionUnderArbitration(); + } + + // Clear any in-progress data from the arbitrator that has now been removed + questionArbitrations[questionId].msg_hash = 0x0; + questionArbitrations[questionId].finalize_ts = 0; + } + + questionArbitrations[questionId].payer = requester; + questionArbitrations[questionId].arbitrator = msg.sender; + + emit LogNotifyOfArbitrationRequest(questionId, requester); + } + + /// @notice Clear the arbitrator setting of an arbitrator that has been delisted + /// @param questionId The question in question + /// @dev Starts the clock ticking to allow us to cancelUnhandledArbitrationRequest + /// @dev Not otherwise needed, if another arbitrator shows up they can just take the job from the delisted arbitrator + function clearRequestFromRemovedArbitrator(bytes32 questionId) external { + address old_arbitrator = questionArbitrations[questionId].arbitrator; + if (old_arbitrator == address(0)) { + revert ArbitratorAddressZero(); + } + if (_arbitrators.contains(old_arbitrator)) { + revert ArbitratorNotRemoved(); + } + + questionArbitrations[questionId].arbitrator = address(0); + questionArbitrations[questionId].msg_hash = 0x0; + questionArbitrations[questionId].finalize_ts = 0; + + questionArbitrations[questionId].last_action_ts = block.timestamp; + } + + /// @notice Cancel the request for arbitration + /// @param questionId The question in question + /// @dev This is only done if nobody takes the request off the queue, probably because the fee is too low + function cancelUnhandledArbitrationRequest(bytes32 questionId) external { + uint256 last_action_ts = questionArbitrations[questionId] + .last_action_ts; + if (last_action_ts == 0) { + revert QuestionNotFound(); + } + if (questionArbitrations[questionId].arbitrator != address(0)) { + revert QuestionAlreadyUnderArbitration(); // Question already accepted by an arbitrator + } + if (block.timestamp - last_action_ts <= QUESTION_UNHANDLED_TIMEOUT) { + revert TooSoonToCancel(); // You can only cancel questions that no arbitrator has accepted in a reasonable time + } + + // Refund the arbitration bounty + balanceOf[questionArbitrations[questionId].payer] = + balanceOf[questionArbitrations[questionId].payer] + + questionArbitrations[questionId].bounty; + delete questionArbitrations[questionId]; + realityETH.cancelArbitration(questionId); + } + + // The arbitrator submits the answer to us, instead of to realityETH + // Instead of sending it to Reality.eth, we instead hold onto it for a challenge period in case someone disputes the arbitrator. + // TODO: We may need assignWinnerAndSubmitAnswerByArbitrator here instead + + /// @notice Submit the arbitrator's answer to a question. + /// @param questionId The question in question + /// @param answer The answer + /// @param answerer The answerer. If arbitration changed the answer, it should be the payer. If not, the old answerer. + /// @dev solc will complain about unsued params but they're used, just via msg.data + function submitAnswerByArbitrator( + bytes32 questionId, + bytes32 answer, + address answerer + ) public { + if (questionArbitrations[questionId].arbitrator != msg.sender) { + revert ArbitratorNotSender(); // An arbitrator can only submit their own arbitration result + } + if (questionArbitrations[questionId].bounty == 0) { + revert QuestionNotInQueue(); // Question not in queue + } + + bytes32 data_hash = keccak256( + abi.encodePacked(questionId, answer, answerer) + ); + uint256 finalize_ts = block.timestamp + ARB_DISPUTE_TIMEOUT; + + questionArbitrations[questionId].msg_hash = data_hash; + questionArbitrations[questionId].finalize_ts = finalize_ts; + } + + /// @notice Resubmit the arbitrator's answer to a question once the challenge period for it has passed + /// @param questionId The question in question + /// @param answer The answer + /// @param answerer The answerer. If arbitration changed the answer, it should be the payer. If not, the old answerer. + function completeArbitration( + bytes32 questionId, + bytes32 answer, + address answerer + ) external { + address arbitrator = questionArbitrations[questionId].arbitrator; + + if (!_arbitrators.contains(arbitrator)) { + revert ArbitratorMustBeAllowlisted(); + } + if (countArbitratorFreezePropositions[arbitrator] != 0) { + revert ArbitratorUnderDispute(); + } + + bytes32 data_hash = keccak256( + abi.encodePacked(questionId, answer, answerer) + ); + if (questionArbitrations[questionId].msg_hash != data_hash) { + revert ResubmissionOfPReviousParameters(); + } + + uint256 finalize_ts = questionArbitrations[questionId].finalize_ts; + if (finalize_ts == 0) { + revert SubmissionMustHaveBeenQueued(); + } + if (finalize_ts > block.timestamp) { + revert ChallengeDeadlineNotPassed(); + } + + balanceOf[questionArbitrations[questionId].payer] = + balanceOf[questionArbitrations[questionId].payer] + + questionArbitrations[questionId].bounty; + + realityETH.submitAnswerByArbitrator(questionId, answer, answerer); + } +} diff --git a/contracts/AdjudicationFramework/Push/AdjudicationFrameworkFeeds.sol b/contracts/AdjudicationFramework/Push/AdjudicationFrameworkFeeds.sol new file mode 100644 index 00000000..c7787f34 --- /dev/null +++ b/contracts/AdjudicationFramework/Push/AdjudicationFrameworkFeeds.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +/* solhint-disable not-rely-on-time */ + +import {MinimalAdjudicationFramework} from "./../MinimalAdjudicationFramework.sol"; +import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; + +/* +This contract is an example contract to govern price feeds using the backstop's arbitration framework. +The feed is stored as an _arbitrator and can be exchanged using the backstop forking method. +*/ + +// Interface copied from here: +// https://github.com/chronicleprotocol/OracleReader-Example/blob/main/src/IChronicle.sol + +interface IChronicle { + /// @notice Returns the oracle's current value. + /// @dev Reverts if no value set. + /// @return value The oracle's current value. + function read() external view returns (uint256 value); +} + +contract AdjudicationFrameworkFeeds is MinimalAdjudicationFramework { + using EnumerableSet for EnumerableSet.AddressSet; + + // @dev Error thrown when non-allowlisted actor tries to call a function + error OracleFrozen(); + + /// @param _realityETH The reality.eth instance we adjudicate for + /// @param _forkArbitrator The arbitrator contract that escalates to an L1 fork, used for our governance + /// @param _initialArbitrators Arbitrator contracts we initially support + constructor( + address _realityETH, + address _forkArbitrator, + address[] memory _initialArbitrators + ) + MinimalAdjudicationFramework( + _realityETH, + _forkArbitrator, + _initialArbitrators, + true // replace method can be used to switch out arbitrators + ) + {} + + function read() public view returns (uint256) { + address oracle = getOracleContract(); + if (countArbitratorFreezePropositions[oracle] > 0) { + revert OracleFrozen(); + } + return IChronicle(oracle).read(); + } + + function getOracleContract() public view returns (address) { + return _arbitrators.at(0); + } +} diff --git a/test/AdjudicationFramework/AdjudicationFrameworkFeeds.t.sol b/test/AdjudicationFramework/AdjudicationFrameworkFeeds.t.sol new file mode 100644 index 00000000..45384d38 --- /dev/null +++ b/test/AdjudicationFramework/AdjudicationFrameworkFeeds.t.sol @@ -0,0 +1,34 @@ +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {AdjudicationFrameworkFeeds} from "../../contracts/AdjudicationFramework/Push/AdjudicationFrameworkFeeds.sol"; +import {RealityETH_v3_0} from "../../contracts/lib/reality-eth/RealityETH-3.0.sol"; + +contract FeedsTest is Test { + AdjudicationFrameworkFeeds public feeds; + address[] public initialArbitrators; + + address public arbitrator1 = address(0x111); + address public arbitrator2 = address(0x222); + address public token = address(0xAAA); + address public realitioMock = address(0x123); + address public l2Arbitrator = address(0x456); + + function setUp() public { + // Setup with initial arbitrators + initialArbitrators = new address[](1); + initialArbitrators[0] = arbitrator1; + + RealityETH_v3_0 l2RealityEth = new RealityETH_v3_0(); + + feeds = new AdjudicationFrameworkFeeds( + address(l2RealityEth), + l2Arbitrator, + initialArbitrators + ); + } + + function testGetOracleContract() public { + assertEq(feeds.getOracleContract(), arbitrator1); + } +} diff --git a/test/AdjudicationFramework/AdjudicationFrameworkMinimal.t.sol b/test/AdjudicationFramework/AdjudicationFrameworkMinimal.t.sol new file mode 100644 index 00000000..7f957f03 --- /dev/null +++ b/test/AdjudicationFramework/AdjudicationFrameworkMinimal.t.sol @@ -0,0 +1,407 @@ +pragma solidity ^0.8.20; + +/* solhint-disable not-rely-on-time */ +/* solhint-disable reentrancy */ +/* solhint-disable quotes */ + +import {Vm} from "forge-std/Vm.sol"; + +import {Test} from "forge-std/Test.sol"; +import {Arbitrator} from "../../contracts/lib/reality-eth/Arbitrator.sol"; + +import {IRealityETH} from "../../contracts/lib/reality-eth/interfaces/IRealityETH.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {ForkableRealityETH_ERC20} from "../../contracts/ForkableRealityETH_ERC20.sol"; +import {RealityETH_v3_0} from "../../contracts/lib/reality-eth/RealityETH-3.0.sol"; +import {AdjudicationFrameworkRequests} from "../../contracts/AdjudicationFramework/Pull/AdjudicationFrameworkRequests.sol"; +import {MinimalAdjudicationFramework} from "../../contracts/AdjudicationFramework/MinimalAdjudicationFramework.sol"; +import {L2ForkArbitrator} from "../../contracts/L2ForkArbitrator.sol"; +import {L1GlobalForkRequester} from "../../contracts/L1GlobalForkRequester.sol"; +import {L2ChainInfo} from "../../contracts/L2ChainInfo.sol"; + +import {MockPolygonZkEVMBridge} from "../testcontract/MockPolygonZkEVMBridge.sol"; + +contract AdjudicationIntegrationTest is Test { + Arbitrator public govArb; + + IERC20 internal tokenMock = + IERC20(0x1234567890123456789012345678901234567890); + + ForkableRealityETH_ERC20 internal l1RealityEth; + RealityETH_v3_0 internal l2RealityEth; + + bytes32 internal addArbitratorQID1; + bytes32 internal addArbitratorQID2; + bytes32 internal removeArbitratorQID1; + bytes32 internal removeArbitratorQID2; + bytes32 internal upgradePropQID1; + bytes32 internal upgradePropQID2; + + AdjudicationFrameworkRequests internal adjudicationFramework1; + AdjudicationFrameworkRequests internal adjudicationFramework2; + + L2ForkArbitrator internal l2ForkArbitrator; + L2ChainInfo internal l2ChainInfo; + + Arbitrator internal l2Arbitrator1; + Arbitrator internal l2Arbitrator2; + + address internal initialArbitrator1 = address(0xbeeb01); + address internal initialArbitrator2 = address(0xbeeb02); + + address internal removeArbitrator1 = address(0xbabe05); + address internal removeArbitrator2 = address(0xbabe06); + + address internal newForkManager1 = address(0xbabe07); + address internal newForkManager2 = address(0xbabe08); + + address payable internal user1 = payable(address(0xbabe09)); + address payable internal user2 = payable(address(0xbabe10)); + + // We'll use a different address to deploy AdjudicationFramework because we want to logs with its address in + address payable internal adjudictionDeployer = payable(address(0xbabe11)); + + string internal constant QUESTION_DELIM = "\u241f"; + + /* + Flow: + - Add/remove arbitrator are requested via the bridge by an AdjudicationFramework on L2. + - Upgrade contracts are requested directly on L1, since L2 may be censored or non-functional. + + TODO: Consider whether we should gate the realityeth instance to approved AdjudicationFramework contracts (via bridge) and an upgrade manager contract. + */ + + uint32 internal constant REALITY_ETH_TIMEOUT = 86400; + + // Dummy addresses for things we message on l1 + // The following should be the same on all forks + MockPolygonZkEVMBridge internal l2Bridge; + address internal l1GlobalForkRequester = + address(new L1GlobalForkRequester()); + address internal l1GlobalChainInfoPublisher = address(0xbabe12); + + // The following will change when we fork so we fake multiple versions here + address internal l1ForkingManager = address(0xbabe13); + address internal l1Token = address(0xbabe14); + + address internal l1ForkingManagerF1 = address(0x1abe13); + address internal l1TokenF1 = address(0x1abe14); + + address internal l1ForkingManagerF2 = address(0x2abe13); + address internal l1TokenF2 = address(0x2abe14); + + uint64 internal l2ChainIdInit = 1; + + uint256 internal forkingFee = 5000; // Should ultimately come from l1 forkingmanager + + function setUp() public { + l2Bridge = new MockPolygonZkEVMBridge(); + + // For now the values of the l1 contracts are all made up + // Ultimately our tests should include a deployment on l1 + l2ChainInfo = new L2ChainInfo( + address(l2Bridge), + l1GlobalChainInfoPublisher + ); + + // Pretend to send the initial setup to the l2 directory via the bridge + // Triggers: + // l2ChainInfo.onMessageReceived(l1GlobalChainInfoPublisher, l1ChainId, fakeMessageData); + // In reality this would originate on L1. + vm.chainId(l2ChainIdInit); + bytes memory fakeMessageData = abi.encode( + l2ChainIdInit, + address(l1ForkingManager), + uint256(forkingFee), + false, + address(l2ForkArbitrator), + bytes32(0x0), + bytes32(0x0) + ); + l2Bridge.fakeClaimMessage( + address(l1GlobalChainInfoPublisher), + uint32(0), + address(l2ChainInfo), + fakeMessageData, + uint256(0) + ); + + l1RealityEth = new ForkableRealityETH_ERC20(); + l1RealityEth.init(tokenMock, address(0), bytes32(0)); + + /* + Creates templates 1, 2, 3 as + TODO: These should probably be special values, or at least not conflict with the standard in-built ones + 1: '{"title": "Should we add arbitrator %s to whitelist contract %s", "type": "bool"}' + 2: '{"title": "Should we remove arbitrator %s from whitelist contract %s", "type": "bool"}' + 3: '{"title": "Should switch to ForkManager %s", "type": "bool"}' + */ + + // Should be a governance arbitrator for adjudicating upgrades + govArb = new Arbitrator(); + govArb.setRealitio(address(l1RealityEth)); + govArb.setDisputeFee(50); + + user1.transfer(1000000); + user2.transfer(1000000); + + // NB we're modelling this on the same chain but it should really be the l2 + l2RealityEth = new RealityETH_v3_0(); + + l2ForkArbitrator = new L2ForkArbitrator( + IRealityETH(l2RealityEth), + L2ChainInfo(l2ChainInfo), + L1GlobalForkRequester(l1GlobalForkRequester), + forkingFee + ); + + // The adjudication framework can act like a regular reality.eth arbitrator. + // It will also use reality.eth to arbitrate its own governance, using the L2ForkArbitrator which makes L1 fork requests. + address[] memory initialArbitrators = new address[](2); + initialArbitrators[0] = initialArbitrator1; + initialArbitrators[1] = initialArbitrator2; + vm.prank(adjudictionDeployer); + adjudicationFramework1 = new AdjudicationFrameworkRequests( + address(l2RealityEth), + 123, + address(l2ForkArbitrator), + initialArbitrators, + true + ); + + l2Arbitrator1 = new Arbitrator(); + // NB The adjudication framework looks to individual arbitrators like a reality.eth question, so they can use it without being changed. + l2Arbitrator1.setRealitio(address(adjudicationFramework1)); + l2Arbitrator1.setDisputeFee(50); + + // Set up another idential arbitrator but don't add them to the framework yet. + l2Arbitrator2 = new Arbitrator(); + l2Arbitrator2.setRealitio(address(adjudicationFramework1)); + l2Arbitrator2.setDisputeFee(50); + + // Create a question - from requestModificationOfArbitrators + // For the setup we'll do this as an uncontested addition. + // Contested cases should also be tested. + addArbitratorQID1 = adjudicationFramework1 + .requestModificationOfArbitrators( + address(0), + address(l2Arbitrator1) + ); + l2RealityEth.submitAnswer{value: 10000}( + addArbitratorQID1, + bytes32(uint256(1)), + 0 + ); + + uint32 to = l2RealityEth.getTimeout(addArbitratorQID1); + assertEq(to, REALITY_ETH_TIMEOUT); + + uint32 finalizeTs = l2RealityEth.getFinalizeTS(addArbitratorQID1); + assertTrue( + finalizeTs > block.timestamp, + "finalization ts should be passed block ts" + ); + + vm.expectRevert("question must be finalized"); + l2RealityEth.resultFor(addArbitratorQID1); + assertTrue( + finalizeTs > block.timestamp, + "finalization ts should be passed block ts" + ); + + vm.expectRevert("question must be finalized"); + adjudicationFramework1.executeModificationArbitratorFromAllowList( + addArbitratorQID1 + ); + + skip(86401); + adjudicationFramework1.executeModificationArbitratorFromAllowList( + addArbitratorQID1 + ); + + assertTrue(adjudicationFramework1.isArbitrator(address(l2Arbitrator1))); + } + + function _simulateRealityEthAnswer( + bytes32 questionId, + bool answer + ) internal { + uint256 answerInt = answer ? 1 : 0; + l2RealityEth.submitAnswer{value: 40000}( + questionId, + bytes32(answerInt), + 0 + ); + skip(86401); + } + + function testInitialArbitrators() public { + // Initial arbitrators from the contructor should be added + assertTrue(adjudicationFramework1.isArbitrator(initialArbitrator1)); + assertTrue(adjudicationFramework1.isArbitrator(initialArbitrator2)); + // This arbitrator may be added in other tests by creating a proposition + assertFalse( + adjudicationFramework1.isArbitrator(address(l2Arbitrator2)) + ); + } + + function testAdjudicationFrameworkTemplateCreation() public { + address[] memory initialArbs; + vm.recordLogs(); + + // Creates 2 templates, each with a log entry from reality.eth + vm.prank(adjudictionDeployer); + new AdjudicationFrameworkRequests( + address(l2RealityEth), + 123, + address(l2ForkArbitrator), + initialArbs, + true + ); + + // NB The length and indexes of this may change if we add unrelated log entries to the AdjudicationFramework constructor + Vm.Log[] memory entries = vm.getRecordedLogs(); + assertEq(entries.length, 3, "Should be 2 log entries"); + + // We should always get the same contract address because we deploy only this with the same user so the address and nonce shouldn't change + string + memory addLog = '{"title": "Should we add the arbitrator %s to the framework 0xfed866a553d106378b828a2e1effb8bed9c9dc28?", "type": "bool", "category": "adjudication", "lang": "en"}'; + string + memory removeLog = '{"title": "Should we remove the arbitrator %s from the framework 0xfed866a553d106378b828a2e1effb8bed9c9dc28?", "type": "bool", "category": "adjudication", "lang": "en"}'; + string + memory replaceLog = '{"title": "Should we replace the arbitrator %s by the new arbitrator %s in the framework 0xfed866a553d106378b828a2e1effb8bed9c9dc28?", "type": "bool", "category": "adjudication", "lang": "en"}'; + + assertEq( + abi.decode(entries[0].data, (string)), + string(removeLog), + "removeLog missing" + ); + assertEq( + abi.decode(entries[1].data, (string)), + string(addLog), + "addLog missing" + ); + assertEq( + abi.decode(entries[2].data, (string)), + string(replaceLog), + "replaceLog missing" + ); + } + + function testrequestModificationOfArbitrators() public { + // Scenario 1: Add 1 arbitrator + + bytes32 questionIdAddMultiple = adjudicationFramework1 + .requestModificationOfArbitrators(address(0), address(0x1000)); + assertNotEq( + questionIdAddMultiple, + bytes32(0), + "Failed to add multiple arbitrators" + ); + + // Scenario 2: Remove an arbitrator + + bytes32 questionIdRemove = adjudicationFramework1 + .requestModificationOfArbitrators(initialArbitrator1, address(0)); + assertNotEq( + questionIdRemove, + bytes32(0), + "Failed to remove arbitrator" + ); + + // Scenario 3: Invalid case - twice the same arbitrators + vm.expectRevert("question must not exist"); + adjudicationFramework1.requestModificationOfArbitrators( + initialArbitrator1, + address(0) + ); + + // Scenario 4: Invalid case - No arbitrators to modify + vm.expectRevert( + MinimalAdjudicationFramework.NoArbitratorsToModify.selector + ); + adjudicationFramework1.requestModificationOfArbitrators( + address(0), + address(0) + ); + } + function testExecuteModificationArbitratorFromAllowList() public { + // Add an arbitrator + + bytes32 questionIdAdd = adjudicationFramework1 + .requestModificationOfArbitrators(address(0), address(0x2000)); + _simulateRealityEthAnswer(questionIdAdd, true); // Assuming this is a helper function to simulate the answer from RealityETH + + adjudicationFramework1.executeModificationArbitratorFromAllowList( + questionIdAdd + ); + assertTrue( + adjudicationFramework1.isArbitrator(address(0x2000)), + "Arbitrator was not added" + ); + + // Remove an arbitrator + bytes32 questionIdRemove = adjudicationFramework1 + .requestModificationOfArbitrators(address(0x2000), address(0)); + _simulateRealityEthAnswer(questionIdRemove, true); + + adjudicationFramework1.executeModificationArbitratorFromAllowList( + questionIdRemove + ); + assertFalse( + adjudicationFramework1.isArbitrator(address(0x2000)), + "Arbitrator was not removed" + ); + } + + function testFreezeArbitratorAndClearFailedProposition() public { + // Freeze an arbitrator + + bytes32 questionId = adjudicationFramework1 + .requestModificationOfArbitrators(initialArbitrator1, address(0)); + + // set temp answer to allow freezeArbitrator() to be called + uint256 tempAnswerInt = 1; + l2RealityEth.submitAnswer{value: 20000}( + questionId, + bytes32(tempAnswerInt), + 0 + ); + vm.expectRevert("question must be finalized"); + adjudicationFramework1.clearFailedProposition(questionId); + + // Assume freezeArbitrator() will be called here with appropriate parameters + adjudicationFramework1.freezeArbitrator( + questionId, + new bytes32[](0), + new address[](0), + new uint256[](0), + new bytes32[](0) + ); + + _simulateRealityEthAnswer(questionId, false); + + // Clear failed proposition + adjudicationFramework1.clearFailedProposition(questionId); + assertFalse( + adjudicationFramework1.isArbitratorPropositionFrozen(questionId), + "Failed to clear failed proposition" + ); + } + + function testclearFailedPropositionCantBeCalledIfPropositionWentThrough() + public + { + // Freeze an arbitrator + bytes32 questionId = adjudicationFramework1 + .requestModificationOfArbitrators(initialArbitrator1, address(0)); + + _simulateRealityEthAnswer(questionId, true); + + // Clear failed proposition + vm.expectRevert( + MinimalAdjudicationFramework.PropositionNotFailed.selector + ); + adjudicationFramework1.clearFailedProposition(questionId); + } +} diff --git a/test/AdjudicationIntegration.t.sol b/test/AdjudicationFramework/AdjudicationFrameworkRequests.t.sol similarity index 82% rename from test/AdjudicationIntegration.t.sol rename to test/AdjudicationFramework/AdjudicationFrameworkRequests.t.sol index 2d563db0..39a20a63 100644 --- a/test/AdjudicationIntegration.t.sol +++ b/test/AdjudicationFramework/AdjudicationFrameworkRequests.t.sol @@ -4,22 +4,19 @@ pragma solidity ^0.8.20; /* solhint-disable reentrancy */ /* solhint-disable quotes */ -import {Vm} from "forge-std/Vm.sol"; - import {Test} from "forge-std/Test.sol"; -import {Arbitrator} from "../contracts/lib/reality-eth/Arbitrator.sol"; - -import {IRealityETH} from "../contracts/lib/reality-eth/interfaces/IRealityETH.sol"; import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; -import {ForkableRealityETH_ERC20} from "../contracts/ForkableRealityETH_ERC20.sol"; -import {RealityETH_v3_0} from "../contracts/lib/reality-eth/RealityETH-3.0.sol"; -import {AdjudicationFramework} from "../contracts/AdjudicationFramework.sol"; - -import {L2ForkArbitrator} from "../contracts/L2ForkArbitrator.sol"; -import {L1GlobalForkRequester} from "../contracts/L1GlobalForkRequester.sol"; -import {L2ChainInfo} from "../contracts/L2ChainInfo.sol"; - -import {MockPolygonZkEVMBridge} from "./testcontract/MockPolygonZkEVMBridge.sol"; +import {ForkableRealityETH_ERC20} from "../../contracts/ForkableRealityETH_ERC20.sol"; +import {AdjudicationFrameworkRequests} from "../../contracts/AdjudicationFramework/Pull/AdjudicationFrameworkRequests.sol"; +import {IRealityETH} from "../../contracts/lib/reality-eth/interfaces/IRealityETH.sol"; +import {RealityETH_v3_0} from "../../contracts/lib/reality-eth/RealityETH-3.0.sol"; +import {Arbitrator} from "../../contracts/lib/reality-eth/Arbitrator.sol"; +import {L2ForkArbitrator} from "../../contracts/L2ForkArbitrator.sol"; +import {L1GlobalForkRequester} from "../../contracts/L1GlobalForkRequester.sol"; +import {L2ChainInfo} from "../../contracts/L2ChainInfo.sol"; +import {MockPolygonZkEVMBridge} from "../testcontract/MockPolygonZkEVMBridge.sol"; +import {MinimalAdjudicationFramework} from "../../contracts/AdjudicationFramework/MinimalAdjudicationFramework.sol"; +import {AdjudicationFrameworkRequests} from "../../contracts/AdjudicationFramework/Pull/AdjudicationFrameworkRequests.sol"; contract AdjudicationIntegrationTest is Test { Arbitrator public govArb; @@ -37,8 +34,8 @@ contract AdjudicationIntegrationTest is Test { bytes32 internal upgradePropQID1; bytes32 internal upgradePropQID2; - AdjudicationFramework internal adjudicationFramework1; - AdjudicationFramework internal adjudicationFramework2; + AdjudicationFrameworkRequests internal adjudicationFramework1; + AdjudicationFrameworkRequests internal adjudicationFramework2; L2ForkArbitrator internal l2ForkArbitrator; L2ChainInfo internal l2ChainInfo; @@ -161,11 +158,12 @@ contract AdjudicationIntegrationTest is Test { initialArbitrators[0] = initialArbitrator1; initialArbitrators[1] = initialArbitrator2; vm.prank(adjudictionDeployer); - adjudicationFramework1 = new AdjudicationFramework( + adjudicationFramework1 = new AdjudicationFrameworkRequests( address(l2RealityEth), 123, address(l2ForkArbitrator), - initialArbitrators + initialArbitrators, + true ); l2Arbitrator1 = new Arbitrator(); @@ -181,9 +179,11 @@ contract AdjudicationIntegrationTest is Test { // Create a question - from beginAddArbitratorToWhitelist // For the setup we'll do this as an uncontested addition. // Contested cases should also be tested. - addArbitratorQID1 = adjudicationFramework1 - .beginAddArbitratorToAllowList(address(l2Arbitrator1)); + .requestModificationOfArbitrators( + address(0), + address(l2Arbitrator1) + ); l2RealityEth.submitAnswer{value: 10000}( addArbitratorQID1, bytes32(uint256(1)), @@ -207,47 +207,16 @@ contract AdjudicationIntegrationTest is Test { ); vm.expectRevert("question must be finalized"); - adjudicationFramework1.executeAddArbitratorToAllowList( + adjudicationFramework1.executeModificationArbitratorFromAllowList( addArbitratorQID1 ); skip(86401); - adjudicationFramework1.executeAddArbitratorToAllowList( + adjudicationFramework1.executeModificationArbitratorFromAllowList( addArbitratorQID1 ); - assertTrue(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); - } - - function testInitialArbitrators() public { - // Initial arbitrators from the contructor should be added - assertTrue(adjudicationFramework1.arbitrators(initialArbitrator1)); - assertTrue(adjudicationFramework1.arbitrators(initialArbitrator2)); - // This arbitrator may be added in other tests by creating a proposition - assertFalse(adjudicationFramework1.arbitrators(address(l2Arbitrator2))); - } - - function testContestedAddArbitrator() public { - addArbitratorQID2 = adjudicationFramework1 - .beginAddArbitratorToAllowList(address(l2Arbitrator2)); - l2RealityEth.submitAnswer{value: 10000}( - addArbitratorQID2, - bytes32(uint256(1)), - 0 - ); - l2RealityEth.submitAnswer{value: 20000}( - addArbitratorQID2, - bytes32(uint256(0)), - 0 - ); - - l2ForkArbitrator.requestArbitration{value: 500000}( - addArbitratorQID2, - 0 - ); - - // This talks to the bridge, we fake what happens next. - // TODO: Hook this up to the real bridge so we can test it properly. + assertTrue(adjudicationFramework1.isArbitrator(address(l2Arbitrator1))); } function _setupContestableQuestion() internal returns (bytes32) { @@ -280,7 +249,9 @@ contract AdjudicationIntegrationTest is Test { ) ); - vm.expectRevert("Arbitrator not allowlisted"); + vm.expectRevert( + MinimalAdjudicationFramework.OnlyAllowlistedActor.selector + ); l2Arbitrator2.requestArbitration{value: 500000}(questionId, 0); } @@ -292,7 +263,9 @@ contract AdjudicationIntegrationTest is Test { assertTrue(l2Arbitrator1.requestArbitration{value: 500000}(qid, 0)); l2Arbitrator1.submitAnswerByArbitrator(qid, bytes32(uint256(1)), user1); - vm.expectRevert("Challenge deadline not passed"); + vm.expectRevert( + AdjudicationFrameworkRequests.ChallengeDeadlineNotPassed.selector + ); adjudicationFramework1.completeArbitration( qid, bytes32(uint256(1)), @@ -331,7 +304,9 @@ contract AdjudicationIntegrationTest is Test { assertTrue(l2Arbitrator1.requestArbitration{value: 500000}(qid, 0)); l2Arbitrator1.submitAnswerByArbitrator(qid, bytes32(uint256(1)), user1); - vm.expectRevert("Challenge deadline not passed"); + vm.expectRevert( + AdjudicationFrameworkRequests.ChallengeDeadlineNotPassed.selector + ); adjudicationFramework1.completeArbitration( qid, bytes32(uint256(1)), @@ -340,7 +315,10 @@ contract AdjudicationIntegrationTest is Test { // now before we can complete this somebody challenges it removalQuestionId = adjudicationFramework1 - .beginRemoveArbitratorFromAllowList(address(l2Arbitrator1)); + .requestModificationOfArbitrators( + address(l2Arbitrator1), + address(0) + ); l2RealityEth.submitAnswer{value: 10000}( removalQuestionId, bytes32(uint256(1)), @@ -352,7 +330,9 @@ contract AdjudicationIntegrationTest is Test { uint256[] memory bonds; bytes32[] memory answers; - vm.expectRevert("Bond too low to freeze"); + vm.expectRevert( + MinimalAdjudicationFramework.BondTooLowToFreeze.selector + ); adjudicationFramework1.freezeArbitrator( removalQuestionId, hashes, @@ -412,13 +392,13 @@ contract AdjudicationIntegrationTest is Test { l2RealityEth.resultFor(removalQuestionId); vm.expectRevert("question must be finalized"); - adjudicationFramework1.executeRemoveArbitratorFromAllowList( + adjudicationFramework1.executeModificationArbitratorFromAllowList( removalQuestionId ); skip(86401); - adjudicationFramework1.executeRemoveArbitratorFromAllowList( + adjudicationFramework1.executeModificationArbitratorFromAllowList( removalQuestionId ); } @@ -440,14 +420,16 @@ contract AdjudicationIntegrationTest is Test { l2RealityEth.resultFor(removalQuestionId); vm.expectRevert("question must be finalized"); - adjudicationFramework1.executeRemoveArbitratorFromAllowList( + adjudicationFramework1.executeModificationArbitratorFromAllowList( removalQuestionId ); skip(86401); - vm.expectRevert("Result was not 1"); - adjudicationFramework1.executeRemoveArbitratorFromAllowList( + vm.expectRevert( + MinimalAdjudicationFramework.PropositionNotAccepted.selector + ); + adjudicationFramework1.executeModificationArbitratorFromAllowList( removalQuestionId ); @@ -543,16 +525,19 @@ contract AdjudicationIntegrationTest is Test { ), 1 ); - assertTrue(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); - adjudicationFramework1.executeRemoveArbitratorFromAllowList( + assertTrue(adjudicationFramework1.isArbitrator(address(l2Arbitrator1))); + adjudicationFramework1.executeModificationArbitratorFromAllowList( removalQuestionId ); - assertFalse(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); + assertFalse( + adjudicationFramework1.isArbitrator(address(l2Arbitrator1)) + ); assertEq( adjudicationFramework1.countArbitratorFreezePropositions( address(l2Arbitrator1) ), - 0 + 0, + "count Arbitrator freeze propositions not correct" ); // TODO: Retry the arbitration with a new arbitrator @@ -636,16 +621,18 @@ contract AdjudicationIntegrationTest is Test { ), 1 ); - assertTrue(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); + assertTrue(adjudicationFramework1.isArbitrator(address(l2Arbitrator1))); - vm.expectRevert("Result was not 1"); - adjudicationFramework1.executeRemoveArbitratorFromAllowList( + vm.expectRevert( + MinimalAdjudicationFramework.PropositionNotAccepted.selector + ); + adjudicationFramework1.executeModificationArbitratorFromAllowList( removalQuestionId ); adjudicationFramework1.clearFailedProposition(removalQuestionId); - assertTrue(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); + assertTrue(adjudicationFramework1.isArbitrator(address(l2Arbitrator1))); assertEq( adjudicationFramework1.countArbitratorFreezePropositions( address(l2Arbitrator1) @@ -729,32 +716,6 @@ contract AdjudicationIntegrationTest is Test { assertEq(user2.balance, user2Bal + forkFee); } - function testAdjudicationFrameworkTemplateCreation() public { - address[] memory initialArbs; - vm.recordLogs(); - - // Creates 2 templates, each with a log entry from reality.eth - vm.prank(adjudictionDeployer); - new AdjudicationFramework( - address(l2RealityEth), - 123, - address(l2ForkArbitrator), - initialArbs - ); - - // NB The length and indexes of this may change if we add unrelated log entries to the AdjudicationFramework constructor - Vm.Log[] memory entries = vm.getRecordedLogs(); - assertEq(entries.length, 2); - - // We should always get the same contract address because we deploy only this with the same user so the address and nonce shouldn't change - string - memory addLog = '{"title": "Should we add arbitrator %s to the framework 0xfed866a553d106378b828a2e1effb8bed9c9dc28?", "type": "bool", "category": "adjudication", "lang": "en"}'; - string - memory removeLog = '{"title": "Should we remove arbitrator %s from the framework 0xfed866a553d106378b828a2e1effb8bed9c9dc28?", "type": "bool", "category": "adjudication", "lang": "en"}'; - assertEq(abi.decode(entries[0].data, (string)), string(addLog)); - assertEq(abi.decode(entries[1].data, (string)), string(removeLog)); - } - /* function testL1RequestGovernanceArbitration() public { bytes32 questionId = keccak256(abi.encodePacked("Question 1")); // TODO: This should be in some wrapper contract