diff --git a/.solhintignore b/.solhintignore index 9f2b1f14..2df1a32f 100644 --- a/.solhintignore +++ b/.solhintignore @@ -1,12 +1,13 @@ -/contracts/RealityETH_ERC20-3.0.sol -/contracts/Arbitrator.sol +/contracts/lib/reality-eth/RealityETH_ERC20-3.0.sol +/contracts/lib/reality-eth/RealityETH-3.0.sol +/contracts/lib/reality-eth/Arbitrator.sol +/contracts/lib/reality-eth/BalanceHolder.sol +/contracts/interfaces/IBalanceHolder.sol +/contracts/interfaces/IBalanceHolder_ERC20.sol /contracts/interfaces/IArbitrator.sol /contracts/interfaces/IRealityETH.sol /contracts/ForkableRealityETH_ERC20.sol /contracts/ForkableRealityETH_ERC20.sol -/contracts/Auction_ERC20.sol -/contracts/mixin/BalanceHolder.sol -/contracts/mixin/BalanceHolder_ERC20.sol /contracts/WhitelistArbitrator.sol /contracts/mixin/Owned.sol /contracts/interfaces/IZKBridge.sol @@ -18,4 +19,3 @@ /contracts/ERC20Mint.sol /contracts/TokenBridge.sol /contracts/ERC20.sol -/contracts/AdjudicationFramework.sol diff --git a/contracts/AdjudicationFramework.sol b/contracts/AdjudicationFramework.sol index a5d7d77c..2d337023 100644 --- a/contracts/AdjudicationFramework.sol +++ b/contracts/AdjudicationFramework.sol @@ -2,52 +2,39 @@ pragma solidity ^0.8.20; -import "./mixin/BalanceHolder.sol"; +/* solhint-disable var-name-mixedcase */ +/* solhint-disable quotes */ +/* solhint-disable not-rely-on-time */ -import "./interfaces/IRealityETH_ERC20.sol"; +import {BalanceHolder} from "./lib/reality-eth/BalanceHolder.sol"; -import "./interfaces/IArbitrator.sol"; -import "./interfaces/IAMB.sol"; -import "./interfaces/IERC20.sol"; +import {IRealityETH} from "./interfaces/IRealityETH.sol"; +import {IArbitrator} from "./interfaces/IArbitrator.sol"; +import {IERC20} from "./interfaces/IERC20.sol"; + +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; /* This contract sits between a Reality.eth instance and an Arbitrator. -It manages a whitelist of arbitrators, and makes sure questions can be sent to an arbitrator on the whitelist. -When called on to arbitrate, it pays someone to send out the arbitration job to an arbitrator on the whitelist. +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 { - // From RealityETH_ERC20 - struct Question { - bytes32 content_hash; - address arbitrator; - uint32 opening_ts; - uint32 timeout; - uint32 finalize_ts; - bool is_pending_arbitration; - uint256 bounty; - bytes32 best_answer; - bytes32 history_hash; - uint256 bond; - uint256 min_bond; - } - mapping(bytes32 => Question) public questions; - - IAMB bridge; uint256 public constant ARB_DISPUTE_TIMEOUT = 86400; uint256 public constant QUESTION_UNHANDLED_TIMEOUT = 86400; - uint256 public constant TOKEN_RESERVATION_BIDDING_PERIOD = 86400; // After you make a bid, people have 1 day to outbid you - uint256 public constant TOKEN_RESERVATION_CLAIM_TIMEOUT = 864000; // After a bid is accepted, you have 9 days to complete it or you can lose your deposit - uint256 public constant TOKEN_RESERVATION_DEPOSIT = 10; // 1/10, ie 10% + 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; - // The bridge (either on L1 or L2) should switch out real L1 forkmanager address for a special address - address constant FORK_MANAGER_SPECIAL_ADDRESS = - 0x00000000000000000000000000000000f0f0F0F0; + uint256 public templateIdAddArbitrator; + uint256 public templateIdRemoveArbitrator; event LogRequestArbitration( bytes32 indexed question_id, @@ -61,22 +48,32 @@ contract AdjudicationFramework is BalanceHolder { address indexed user ); - struct TokenReservation { - address reserver; - uint256 num; - uint256 price; - uint256 reserved_ts; + // AllowList of acceptable arbitrators + mapping(address => bool) public arbitrators; + + enum PropositionType { + NONE, + ADD_ARBITRATOR, + REMOVE_ARBITRATOR, + UPGRADE_BRIDGE } - mapping(bytes32 => TokenReservation) public token_reservations; - uint256 public reserved_tokens; - // Allowlist of acceptable arbitrators - mapping(address => bool) public arbitrators; + // 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; - // List of arbitrators that are currently being challenged - mapping(address => bool) public frozen_arbitrators; + // 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_ERC20 public realityETH; + IRealityETH public realityETH; + + // Arbitrator used for requesting a fork in the L1 chain in add/remove propositions + address public forkArbitrator; uint256 public dispute_fee; @@ -91,40 +88,50 @@ contract AdjudicationFramework is BalanceHolder { mapping(bytes32 => ArbitrationRequest) public question_arbitrations; - // TODO: Work out how this is implemented in xdai or whatever we use - modifier l1_forkmanager_only() { - require(msg.sender == address(bridge), "Message must come from bridge"); - require( - bridge.messageSender() == FORK_MANAGER_SPECIAL_ADDRESS, - "Message must come from L1 ForkManager" - ); - _; - } - + /// @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, - IAMB _bridge, - address[] memory _initial_arbitrators + address _forkArbitrator, + address[] memory _initialArbitrators ) { - realityETH = IRealityETH_ERC20(_realityETH); + realityETH = IRealityETH(_realityETH); dispute_fee = _dispute_fee; - bridge = _bridge; - for (uint256 i = 0; i < _initial_arbitrators.length; i++) { - arbitrators[_initial_arbitrators[i]] = true; + 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: make it depend on the question + // TODO: Should we have a governance process to change this? return dispute_fee; } /// @notice Request arbitration, freezing the question until we send submitAnswerByArbitrator - /// @dev The bounty can be paid only in part, in which case the last person to pay will be considered the payer - /// Will trigger an error if the notification fails, eg because the question has already been finalized + /// @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( @@ -134,9 +141,9 @@ contract AdjudicationFramework is BalanceHolder { uint256 arbitration_fee = getDisputeFee(question_id); require( arbitration_fee > 0, - "The arbitrator must have set a non-zero fee for the question" + "Question must have fee" // "The arbitrator must have set a non-zero fee for the question" ); - require(msg.value >= arbitration_fee); + require(msg.value >= arbitration_fee, "Insufficient fee"); realityETH.notifyOfArbitrationRequest( question_id, @@ -145,8 +152,8 @@ contract AdjudicationFramework is BalanceHolder { ); emit LogRequestArbitration(question_id, msg.value, msg.sender, 0); - // Queue the question for arbitration by a whitelisted arbitrator - // Anybody can take the question off the queue and submit it to a whitelisted arbitrator + // 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 @@ -169,17 +176,17 @@ contract AdjudicationFramework is BalanceHolder { address requester, uint256 ) external { - require(arbitrators[msg.sender], "Arbitrator must be on the whitelist"); + require(arbitrators[msg.sender], "Arbitrator not allowlisted"); require( question_arbitrations[question_id].bounty > 0, - "Question must be in the arbitration queue" + "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 whitelist + // 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 already taken, and the arbitrator who took it is still active" + "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 @@ -202,7 +209,7 @@ contract AdjudicationFramework is BalanceHolder { require(old_arbitrator != address(0), "No arbitrator to remove"); require( !arbitrators[old_arbitrator], - "Arbitrator must no longer be on the whitelist" + "Arbitrator not removed" // Arbitrator must no longer be on the allowlist ); question_arbitrations[question_id].arbitrator = address(0); @@ -222,11 +229,11 @@ contract AdjudicationFramework is BalanceHolder { require( question_arbitrations[question_id].arbitrator == address(0), - "Question already accepted by an arbitrator" + "Already under arbitration" // Question already accepted by an arbitrator ); require( block.timestamp - last_action_ts > QUESTION_UNHANDLED_TIMEOUT, - "You can only cancel questions that no arbitrator has accepted in a reasonable time" + "Too soon to cancel" // You can only cancel questions that no arbitrator has accepted in a reasonable time ); // Refund the arbitration bounty @@ -253,11 +260,11 @@ contract AdjudicationFramework is BalanceHolder { ) public { require( question_arbitrations[question_id].arbitrator == msg.sender, - "An arbitrator can only submit their own arbitration result" + "Sender not the arbitrator" // An arbitrator can only submit their own arbitration result ); require( question_arbitrations[question_id].bounty > 0, - "Question must be in the arbitration queue" + "Question not in queue" ); bytes32 data_hash = keccak256( @@ -280,10 +287,10 @@ contract AdjudicationFramework is BalanceHolder { ) external { address arbitrator = question_arbitrations[question_id].arbitrator; - require(arbitrators[arbitrator], "Arbitrator must be whitelisted"); + require(arbitrators[arbitrator], "Arbitrator must be allowlisted"); require( - !frozen_arbitrators[arbitrator], - "Arbitrator must not be under dispute" + countArbitratorFreezePropositions[arbitrator] == 0, + "Arbitrator under dispute" ); bytes32 data_hash = keccak256( @@ -291,14 +298,14 @@ contract AdjudicationFramework is BalanceHolder { ); require( question_arbitrations[question_id].msg_hash == data_hash, - "You must resubmit the parameters previously sent" + "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 must have passed" + "Challenge deadline not passed" ); balanceOf[question_arbitrations[question_id].payer] = @@ -308,133 +315,114 @@ contract AdjudicationFramework is BalanceHolder { realityETH.submitAnswerByArbitrator(question_id, answer, answerer); } - function freezeArbitrator( - address arbitrator // l1_forkmanager_only - ) public { - require( - arbitrators[arbitrator], - "Arbitrator not whitelisted in the first place" - ); - require(!frozen_arbitrators[arbitrator], "Arbitrator already frozen"); - frozen_arbitrators[arbitrator] = true; + // 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 unfreezeArbitrator(address arbitrator) public l1_forkmanager_only { - require( - arbitrators[arbitrator], - "Arbitrator not whitelisted in the first place" - ); - require( - frozen_arbitrators[arbitrator], - "Arbitrator not already frozen" - ); - frozen_arbitrators[arbitrator] = false; + 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 addArbitrator(address arbitrator) public l1_forkmanager_only { - require(!arbitrators[arbitrator], "Arbitrator already whitelisted"); + 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 removeArbitrator(address arbitrator) public l1_forkmanager_only { - require(arbitrators[arbitrator], "Arbitrator already whitelisted"); - frozen_arbitrators[arbitrator] = false; - arbitrators[arbitrator] = false; - } + function executeRemoveArbitratorFromAllowList(bytes32 question_id) external { - function _numUnreservedTokens() internal view returns (uint256) { - return address(this).balance - reserved_tokens; - } + require(propositions[question_id].proposition_type == PropositionType.REMOVE_ARBITRATOR, "Wrong Proposition type"); - function reserveTokens(uint256 num, uint256 price, uint256 nonce) public { - bytes32 resid = keccak256(abi.encodePacked(msg.sender, nonce)); - require( - token_reservations[resid].reserved_ts == 0, - "Nonce already used" - ); + // 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. - require(_numUnreservedTokens() > num, "Not enough tokens unreserved"); + address arbitrator = propositions[question_id].arbitrator; + bytes32 realityEthResult = realityETH.resultFor(question_id); + require(realityEthResult == bytes32(uint256(1)), "Result was not 1"); - uint256 deposit = (num * price) / TOKEN_RESERVATION_DEPOSIT; - payable(msg.sender).transfer(deposit); + if (propositions[question_id].isFrozen) { + countArbitratorFreezePropositions[arbitrator] = countArbitratorFreezePropositions[arbitrator] - 1; + } + delete(propositions[question_id]); - token_reservations[resid] = TokenReservation( - msg.sender, - num, - price, - block.timestamp - ); - reserved_tokens = reserved_tokens + num; + arbitrators[arbitrator] = false; } - function outBidReservation( - uint256 num, - uint256 price, - uint256 nonce, - bytes32 resid - ) external { - require( - token_reservations[resid].reserved_ts > 0, - "Reservation you want to outbid does not exist" - ); - uint256 age = block.timestamp - token_reservations[resid].reserved_ts; - require( - age < TOKEN_RESERVATION_BIDDING_PERIOD, - "Bidding period has passed" - ); + // 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( - token_reservations[resid].num >= num, - "More tokens requested than remain in the reservation" - ); - require( - price > (token_reservations[resid].price * 101) / 100, - "You must bid at least 1% more than the previous bid" + arbitrators[arbitrator], + "Arbitrator not allowlisted" // Not allowlisted in the first place ); + require(!propositions[question_id].isFrozen, "Arbitrator already frozen"); - uint256 deposit_return = (num * token_reservations[resid].price) / - TOKEN_RESERVATION_DEPOSIT; + // 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 - payable(token_reservations[resid].reserver).transfer(deposit_return); - reserved_tokens = reserved_tokens - num; - - if (num == token_reservations[resid].num) { - delete (token_reservations[resid]); + 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 { - token_reservations[resid].num = token_reservations[resid].num - num; + (answer, bond) = realityETH.getEarliestAnswerFromSuppliedHistoryOrRevert(question_id, history_hashes, addrs, bonds, answers); } - return reserveTokens(num, price, nonce); - } - - function cancelTimedOutReservation(bytes32 resid) external { - uint256 age = block.timestamp - token_reservations[resid].reserved_ts; - require(age > TOKEN_RESERVATION_CLAIM_TIMEOUT, "Not timed out yet"); - reserved_tokens = reserved_tokens - token_reservations[resid].num; - delete (token_reservations[resid]); - } + require(answer == bytes32(uint256(1)), "Supplied answer is not yes"); + require(bond >= REALITY_ETH_BOND_ARBITRATOR_FREEZE, "Bond too low to freeze"); - function executeTokenSale( - bytes32 resid, - uint256 gov_tokens_paid - ) external l1_forkmanager_only { - uint256 age = block.timestamp - token_reservations[resid].reserved_ts; - require( - age > TOKEN_RESERVATION_BIDDING_PERIOD, - "Bidding period has not yet passed" - ); + // TODO: Ideally we would check the bond is for the "remove" answer. + // #92 - uint256 num = token_reservations[resid].num; - uint256 price = token_reservations[resid].price; - uint256 cost = price * num; - require(gov_tokens_paid >= cost, "Insufficient gov tokens sent"); - reserved_tokens = reserved_tokens - num; - payable(token_reservations[resid].reserver).transfer(num); + propositions[question_id].isFrozen = true; + countArbitratorFreezePropositions[arbitrator] = countArbitratorFreezePropositions[arbitrator] + 1; + } - delete (token_reservations[resid]); + 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/Auction_ERC20.sol b/contracts/Auction_ERC20.sol deleted file mode 100644 index 7b97b295..00000000 --- a/contracts/Auction_ERC20.sol +++ /dev/null @@ -1,223 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -/* -There's a gas token on L2. -This is bridged to an ERC20 on L1, called ForkManager. - -After the fork, there will be 2 new tokens, and people with balances on the original token will be able to migrate them to the 2 new tokens. -For 1 week before the fork, there will be an incentivized auction where people can bid whatever they think is the correct valuation of the 2 tokens. -They will then be paid out in one or the other of the tokens, at the rate they bid or better. -eg if you say the value of A:B splits 80:20, you will receive at least either 5 of B or 1.25 of A. -*/ - -pragma solidity ^0.8.20; - -contract Auction_ERC20 { - // todo: rething the name: actually there are 101 slots - uint256 constant MAX_SLOTS = 100; - uint256 public bidCounter; - - uint256 public bonus; - uint256 public forkTimestamp; - address public forkmanager; - - struct Bid { - address owner; - uint8 bid; // bid price, 0:100,1:99 to 100:0 ratio of fork prices - uint256 amount; - } - - // maps the bidPrice to the cumulative amount of tokens bid at that price - mapping(uint8 => uint256) public cumulativeBids; - // maps the bidCounter to the bid - mapping(uint256 => Bid) public bids; - - bool public isCalculationDone; - uint8 public finalPrice; - uint256 public bonusRatio; - - // If you bid exactly on the price at which we settled, you can claim whichever your prefer on a first-come-first-serve basis. - uint256 public tiedYesTokensRemain; - uint256 public tiedNoTokensRemain; - - event LogBid(uint256 bidCounter, address payee, uint8 bid, uint256 value); - - event LogChangeBid( - uint256 bidCounter, - address payee, - uint8 oldBid, - uint8 newBid, - uint256 value - ); - - modifier beforeFork() { - require(block.timestamp < forkTimestamp, "must be before fork"); - _; - } - - modifier onlyForkManager() { - require(msg.sender == forkmanager, "Call via the forkmanager"); - _; - } - - modifier afterForkBeforeCalculation() { - require(block.timestamp >= forkTimestamp, "must be after fork"); - require(!isCalculationDone, "price calculation already done"); - _; - } - - modifier afterForkAfterCalculation() { - require(isCalculationDone, "must be after price calculation"); - _; - } - - // ForkManager should call this on deployment and credit this contract with the bonus amount - // Todo: maybe this should be a constructor? - function init(uint256 _bonus, uint256 _forkTimestamp) external { - require(forkmanager == address(0), "Already initialized"); - forkmanager = msg.sender; - bonus = _bonus; - forkTimestamp = _forkTimestamp; - } - - // ForkManager should lock the tokens before calling this - function bid( - address owner, - uint8 _bid, - uint256 _amount - ) external beforeFork onlyForkManager { - require(_bid <= MAX_SLOTS); - require(owner != address(0), "Owner not set"); - - bidCounter = bidCounter + 1; - bids[bidCounter] = Bid(owner, _bid, _amount); - cumulativeBids[_bid] = cumulativeBids[_bid] + _amount; - emit LogBid(bidCounter, owner, _bid, _amount); - } - - function changeBid(uint256 _bidCounter, uint8 newBid) public beforeFork { - require(newBid <= MAX_SLOTS, "bid higher than MAX_SLOTS"); - address owner = bids[_bidCounter].owner; - require(owner == msg.sender, "You can only change your own bid"); - uint256 value = bids[_bidCounter].amount; - uint8 oldBid = bids[_bidCounter].bid; - bids[bidCounter].bid = newBid; - cumulativeBids[oldBid] = cumulativeBids[oldBid] - value; - cumulativeBids[newBid] = cumulativeBids[newBid] + value; - emit LogChangeBid(bidCounter, owner, oldBid, newBid, value); - } - - function getTotalBids() public view returns (uint256 total) { - for (uint8 i = 0; i <= MAX_SLOTS; i++) { - total = total + cumulativeBids[i]; - } - } - - function calculatePrice() public afterForkBeforeCalculation { - uint256 totalBids = getTotalBids(); - - // eg bonus is 100, total is 2000, you get an extra 1/20 - bonusRatio = totalBids / bonus; - uint256 sumBids = 0; - - /* - Example of price calculation with 200 tokens - 10/90: 10 - cumulative 10, multipler 100/10=10, uses 100 tokens - 20/80: 10 - cumulative 20, multipler 100/20= 5, uses 100 tokens - 30/70: 50 - cumulative 70, multipler 100/30=3.3, uses 233 tokens, done - 60/40: 20 - 80/20: 10 - */ - - sumBids = cumulativeBids[0]; - for (uint8 i = 1; i <= MAX_SLOTS; i++) { - sumBids = sumBids + cumulativeBids[i]; - uint256 tokensNeeded = ((sumBids * MAX_SLOTS) / i); - if (tokensNeeded >= totalBids) { - finalPrice = i; - isCalculationDone = true; - - /* - eg we split 60/40 but then the 60 side had 50 tokens, satisfying them all required 210 tokens and there are only 200 - If that happens, assign the excess (10) to the no side and let people claim from whichever side they prefer until there are none left - */ - - uint256 excess = tokensNeeded - totalBids; - uint256 tokensNeededForThisBidPrice = ((cumulativeBids[i] * - MAX_SLOTS) / i); - - tiedNoTokensRemain = excess; - tiedYesTokensRemain = tokensNeededForThisBidPrice - excess; - break; - } - } - } - - // todo: deal with the case where the price is 50% and both are winners? - // maybe a function called: IsMajorityWinner() might more appropriated - function winner() external view afterForkAfterCalculation returns (bool) { - return (finalPrice * 2 > MAX_SLOTS); - } - - // Call settleAuction(bid, yesOrNo) against the ForkManager - // This will read the amount that needs to be paid out, clear it so it isn't paid twice, and mint the tokens in the appropriate token. - // Usually this would be called by whoever made the bid, but anyone is allowed to call it. - // There's usually only one option for yesOrNo that won't revert, unless you bid exactly at the setotalBidsement price in which case you may be able to choose. - function clearAndReturnPayout( - uint256 _bidCounter, - bool yesOrNo - ) - public - onlyForkManager - afterForkAfterCalculation - returns (address, uint256) - { - require(bids[_bidCounter].owner != address(0), "Bid not found"); - uint256 bidAmount = bids[_bidCounter].bid; - uint256 due; - address payee = bids[_bidCounter].owner; - - if (yesOrNo) { - due = (bidAmount * MAX_SLOTS) / finalPrice; - } else { - due = (bidAmount * MAX_SLOTS) / (MAX_SLOTS - finalPrice); - } - - if (bidAmount == finalPrice) { - // If it's a tie, we can only allocate as much as remains available. - - uint256 willPay = due; - if (yesOrNo && due > tiedYesTokensRemain) { - willPay = tiedYesTokensRemain; - } else if (!yesOrNo && due > tiedNoTokensRemain) { - willPay = tiedNoTokensRemain; - } - require(willPay > 0, "No tokens to claim"); - if (willPay < due) { - // Reduce the remaining bid amount by the proportion of the amount we were unable to fill on the requested side - bids[_bidCounter].amount = uint88( - bidAmount - ((bidAmount * willPay) / due) - ); - } else { - delete (bids[_bidCounter]); - } - - if (yesOrNo) { - tiedYesTokensRemain = tiedYesTokensRemain - willPay; - } else { - tiedNoTokensRemain = tiedNoTokensRemain - willPay; - } - - due = willPay; - } else { - require( - (bidAmount > finalPrice) == yesOrNo, - "You can only get yes if you bid same or higher, no same or lower" - ); - delete (bids[_bidCounter]); - } - - due = due + (due / bonusRatio); - return (payee, due); - } -} diff --git a/contracts/ForkableRealityETH_ERC20.sol b/contracts/ForkableRealityETH_ERC20.sol index 455fb349..a1c5ace0 100644 --- a/contracts/ForkableRealityETH_ERC20.sol +++ b/contracts/ForkableRealityETH_ERC20.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; -import "./mixin/BalanceHolder_ERC20.sol"; +import "./lib/reality-eth/BalanceHolder_ERC20.sol"; import "./interfaces/IForkableRealityETH.sol"; diff --git a/contracts/ForkingManager.sol b/contracts/ForkingManager.sol index 6521671f..521c477d 100644 --- a/contracts/ForkingManager.sol +++ b/contracts/ForkingManager.sol @@ -41,7 +41,6 @@ contract ForkingManager is IForkingManager, ForkableStructure { DisputeData public disputeData; NewImplementations public proposedImplementations; uint256 public executionTimeForProposal = 0; - uint256 public immutable forkPreparationTime = 1 weeks; /// @inheritdoc IForkingManager @@ -64,6 +63,19 @@ contract ForkingManager is IForkingManager, ForkableStructure { ForkableStructure.initialize(address(this), _parentContract); } + function isForkingInitiated() external view returns (bool) { + return (executionTimeForProposal > 0); + } + + function isForkingExecuted() external view returns (bool) { + return (children[0] != address(0) || children[1] != address(0)); + } + + // TODO: If there is any other reason a fork can be prevented, eg the contract can be frozen by governance, add it here. + function canFork() external view returns (bool) { + return (executionTimeForProposal == 0); + } + /** * @notice function to initiate and schedule the fork * @param _disputeData the dispute contract and call to identify the dispute @@ -165,7 +177,7 @@ contract ForkingManager is IForkingManager, ForkableStructure { ); initializePackedParameters.chainID = ChainIdManager(chainIdManager) .getNextUsableChainId(); - initializePackedParameters.forkID = newImplementations.forkID; + initializePackedParameters.forkID = newImplementations.forkID > 0 ? newImplementations.forkID : IPolygonZkEVM(zkEVM).forkID(); IForkableZkEVM(newInstances.zkEVM.two).initialize( newInstances.forkingManager.two, zkEVM, @@ -260,4 +272,5 @@ contract ForkingManager is IForkingManager, ForkableStructure { newInstances.bridge.two ); } + } diff --git a/contracts/L1GlobalChainInfoPublisher.sol b/contracts/L1GlobalChainInfoPublisher.sol new file mode 100644 index 00000000..ee48f6fd --- /dev/null +++ b/contracts/L1GlobalChainInfoPublisher.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +import {IForkingManager} from "./interfaces/IForkingManager.sol"; +import {IForkableStructure} from "./interfaces/IForkableStructure.sol"; +import {IPolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMBridge.sol"; +import {IPolygonZkEVM} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVM.sol"; + +contract L1GlobalChainInfoPublisher { + + /// @notice Function to send the data about a fork to a contract on L2. + /// @param _bridge The bridge to send the data through + /// @param _l2ChainInfo The L2ChainInfo contract on L2 to send the data to + /// @param _ancestorForkingManager The ForkingManager to send data about, if referring to a previous fork (unusual) + /// @param _maxAncestors The number of forks back to look when looking for the _ancestorForkingManager + /// @dev Normally someone would call this right after a fork, _ancestorForkingManager and _maxAncestors should only be used in wierd cases + function updateL2ChainInfo(address _bridge, address _l2ChainInfo, address _ancestorForkingManager, uint256 _maxAncestors) external { + + // Ask the bridge its forkmanager + IForkingManager forkingManager = IForkingManager(IForkableStructure(_bridge).forkmanager()); + + // If we passed an _ancestorForkingManager, crawl up and find that as our ancestor and send data for that over the current bridge. + // We will refuse to send data about a forkingManager that isn't an ancestor of the one used by the bridge. + // Normally we won't need to do this because we'll update L2ChainInfo as soon as there's a fork + // This is here just in case there's some weird availability issue and we couldn't send an update before the next fork. + // NB If we keep forking every week forever you will eventually become unable to get the earliest before running out of gas + if (_ancestorForkingManager != address(0)) { + bool found = false; + for(uint256 i = 0; i < _maxAncestors; i++) { + forkingManager = IForkingManager(forkingManager.parentContract()); + if (address(forkingManager) == address(0)) { + break; // No more ancestors + } + if (_ancestorForkingManager == address(forkingManager)) { + found = true; + break; + } + } + require(found, "Ancestor not found"); + } + + // Dispute results will need to come from the parent ForkingManager + IForkingManager parentForkingManager = IForkingManager(forkingManager.parentContract()); + + bool isL1; + address disputeContract; + bytes32 disputeContent; + + // Fork results: 0 for the genesis, 1 for yes, 2 for no + uint8 forkResult = 0; + + // Find out whether we are the "yes" fork or the "no" fork + if (address(parentForkingManager) != address(0)) { + (address child1, address child2) = parentForkingManager.getChildren(); + if (child1 == address(forkingManager)) { + forkResult = 1; + } else if (child2 == address(forkingManager)) { + forkResult = 2; + } else { + revert("Unexpected child address"); + } + (isL1, disputeContract, disputeContent) = forkingManager.disputeData(); + } + + uint256 arbitrationFee = forkingManager.arbitrationFee(); + address forkonomicToken = forkingManager.forkonomicToken(); + uint64 chainId = IPolygonZkEVM(forkingManager.zkEVM()).chainID(); + + bytes memory data = abi.encode(chainId, forkonomicToken, arbitrationFee, isL1, disputeContract, disputeContent, forkResult); + + IPolygonZkEVMBridge(_bridge).bridgeMessage( + uint32(1), + _l2ChainInfo, + true, + data + ); + } + +} diff --git a/contracts/L1GlobalForkRequester.sol b/contracts/L1GlobalForkRequester.sol new file mode 100644 index 00000000..ed4e333b --- /dev/null +++ b/contracts/L1GlobalForkRequester.sol @@ -0,0 +1,177 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +/* + Contract to proxy a fork request from the bridge to its ForkingManager + Any L2 contract can call us over the bridge to get us to request a fork. + We record the dispute they were forking over +*/ + +import {IForkingManager} from "./interfaces/IForkingManager.sol"; +import {IPolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMBridge.sol"; +import {IPolygonZkEVM} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVM.sol"; +import {IForkonomicToken} from "./interfaces/IForkonomicToken.sol"; + +// NB We'd normally use the interface IForkableBridge here but it causes an error: +// Error (5005): Linearization of inheritance graph impossible +import {ForkableBridge} from "./ForkableBridge.sol"; + +import {IBridgeMessageReceiver} from "@RealityETH/zkevm-contracts/contracts/interfaces/IBridgeMessageReceiver.sol"; + +import {MoneyBox} from "./mixin/MoneyBox.sol"; +import {CalculateMoneyBoxAddress} from "./lib/CalculateMoneyBoxAddress.sol"; + +contract L1GlobalForkRequester { + + struct FailedForkRequest { + uint256 amount; + uint256 amountMigratedYes; + uint256 amountMigratedNo; + } + // Token => Beneficiary => ID => FailedForkRequest + mapping(address=>mapping(address=>mapping(bytes32=>FailedForkRequest))) public failedRequests; + + // Anybody can say, "Hey, you got a payment for this fork to happen" + // Normally this would only happen if the L2 contract send a payment but in theory someone else could fund it directly on L1. + function handlePayment(address token, address beneficiary, bytes32 requestId) external { + + // Normally the "beneficiary" would be the sender on L2. + // But if some other kind person sent funds to this address we would still send it back to the beneficiary. + + bytes32 salt = keccak256(abi.encodePacked(beneficiary, requestId)); + + // Check the MoneyBox has funds + address moneyBox = CalculateMoneyBoxAddress._calculateMoneyBoxAddress(address(this), salt, token); + + // If for some reason we've already got part of a payment, include it. + uint256 initialBalance = failedRequests[token][beneficiary][requestId].amount; + + uint256 transferredBalance = initialBalance + IForkonomicToken(token).balanceOf(moneyBox); + + if (moneyBox.code.length == 0) { + new MoneyBox{salt: salt}(token); + } + require(IForkonomicToken(token).transferFrom(moneyBox, address(this), transferredBalance), "Preparing payment failed"); + + // If the token is already being or has already been forked, record the request as failed. + // Somebody can split the token after the fork, then send the failure message and the funds back on both the child forks. + // TODO: Replace this with an isForked() method in ForkingStructure.sol? + + IForkingManager forkingManager = IForkingManager(IForkonomicToken(token).forkmanager()); + + bool isForkGuaranteedNotToRevert = true; + if (transferredBalance < forkingManager.arbitrationFee()) { + isForkGuaranteedNotToRevert = false; + } + if (!forkingManager.canFork()) { + isForkGuaranteedNotToRevert = false; + } + + if (isForkGuaranteedNotToRevert) { + + if (initialBalance > 0) { + delete(failedRequests[token][beneficiary][requestId]); + } + + IForkonomicToken(token).approve(address(forkingManager), transferredBalance); + + // Assume the data contains the questionId and pass it directly to the forkmanager in the fork request + IForkingManager.NewImplementations memory newImplementations; + IForkingManager.DisputeData memory disputeData = IForkingManager.DisputeData(false, beneficiary, requestId); + forkingManager.initiateFork(disputeData, newImplementations); + + } else { + + // Store the request so we can return the tokens across the bridge + // If the fork has already happened we may have to split them first and do this twice. + failedRequests[token][beneficiary][requestId].amount = transferredBalance; + + } + + } + + // If something was queued after a fork had happened, we need to be able to return then to both bridges + function splitTokensIntoChildTokens(address token, address requester, bytes32 requestId) external { + //function splitTokensIntoChildTokens(address token, address requester, bytes32 requestId, bool doYesToken, bool doNoToken) external {} + // TODO: We need to update ForkonomicToken to handle each side separately in case one bridge reverts maliciously. + // Then handle only one side being requested, or only one side being left + uint256 amount = failedRequests[token][requester][requestId].amount; + + // You need to call registerPayment before you call this. + require(amount > 0, "Nothing to split"); + + (address yesToken, address noToken) = IForkonomicToken(token).getChildren(); + require(yesToken != address(0) && noToken != address(0), "Token not forked"); + + // Current version only has a single function so we have to migrate both sides + IForkonomicToken(token).splitTokensIntoChildTokens(amount); + failedRequests[yesToken][requester][requestId].amount += amount; + failedRequests[noToken][requester][requestId].amount += amount; + delete(failedRequests[token][requester][requestId]); + + /* + // Probably need something like: + + uint256 amountRemainingY = amount - failedRequests[token][requester][requestId].amountMigratedYes; + uint256 amountRemainingN = amount - failedRequests[token][requester][requestId].amountMigratedNo; + + if (doYesToken) { + require(amountRemainingY > 0, "Nothing to migrate for Y"); + token.splitTokensIntoChildTokens(amountRemainingY, 1); + amountRemainingY = 0; + } + + if (doNoToken) { + require(amountMigratedNo > 0, "Nothing to migrate for N"); + token.splitTokensIntoChildTokens(amountMigratedNo, 0); + amountRemainingN = 0; + } + + if (amountRemainingY == 0 && amountRemainingN == 0) { + delete(failedRequests[token][requester][requestId]); + } else { + failedRequests[token][requester][requestId].amountRemainingY = amountRemainingY; + failedRequests[token][requester][requestId].amountRemainingN = amountRemainingN; + } + */ + } + + function returnTokens(address token, address requester, bytes32 requestId) external { + + IForkingManager forkingManager = IForkingManager(IForkonomicToken(token).forkmanager()); + ForkableBridge bridge = ForkableBridge(forkingManager.bridge()); + + // Check the relations in the other direction to make sure we don't lie to the bridge somehow + require(address(bridge.forkmanager()) == address(forkingManager), "Bridge/manager mismatch, WTF"); + require(address(forkingManager.forkonomicToken()) == token, "Token/manager mismatch, WTF"); + + uint256 amount = failedRequests[token][requester][requestId].amount; + + require(amount > 0, "Nothing to return"); + IForkonomicToken(token).approve(address(bridge), amount); + + bytes memory permitData; + bridge.bridgeAsset( + uint32(1), + requester, + amount, + token, // TODO: Should this be address(0)? + true, + permitData + ); + + // TODO: It might be useful to send information about the failure eg fork timestamp + bytes memory data = bytes.concat(requestId); + bridge.bridgeMessage( + uint32(1), + requester, + true, + data + ); + + delete(failedRequests[token][requester][requestId]); + + } + +} diff --git a/contracts/L2ChainInfo.sol b/contracts/L2ChainInfo.sol new file mode 100644 index 00000000..b14cde48 --- /dev/null +++ b/contracts/L2ChainInfo.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +/* +This contract lives on L2 and shares information about the chain. +It needs to get this information by being called after a fork. +We made it for the ForkArbitrator to get the result after a fork. +Other contracts may also find it useful. +It must be called after a fork until it's updated. +Queries against it will revert until the update is done. +*/ + +import {IBridgeMessageReceiver} from "@RealityETH/zkevm-contracts/contracts/interfaces/IBridgeMessageReceiver.sol"; + +contract L2ChainInfo is IBridgeMessageReceiver{ + + // These are the same for all forks + address public l2Bridge; + address public l1GlobalChainInfoPublisher; + uint32 public constant L1_NETWORK_ID = 0; + + struct ChainInfo{ + address forkonomicToken; + uint256 forkFee; + } + mapping(uint64 => ChainInfo) public chainInfo; + + // Questions are stored by isL2->forker->id + // The forker is assumed to have created a unique id within itself for the dispute it's forking over + mapping(bool=>mapping(address=>mapping(bytes32=>bytes32))) public forkQuestionResults; + mapping(bool=>mapping(address=>mapping(bytes32=>uint64))) public questionToChainID; + + constructor(address _l2Bridge, address _l1GlobalChainInfoPublisher) { + l2Bridge = _l2Bridge; + l1GlobalChainInfoPublisher = _l1GlobalChainInfoPublisher; + } + + modifier isUpToDate { + require(chainInfo[uint64(block.chainid)].forkonomicToken != address(0), "Current chain must be known"); + _; + } + + function getForkonomicToken() external view isUpToDate returns(address) { + return chainInfo[uint64(block.chainid)].forkonomicToken; + } + + function getForkFee() external view isUpToDate returns(uint256) { + return chainInfo[uint64(block.chainid)].forkFee; + } + + function getForkQuestionResult(bool isL1, address forker, bytes32 questionId) external view isUpToDate returns(bytes32) { + return forkQuestionResults[isL1][forker][questionId]; + } + + // Get a message via the bridge from a contract we trust on L1 reporting to us the details of a fork. + // It should normally be used right after a fork to send us the current chain. + // It could also send us information about a previous chain that's a parent of ours if we forked again before getting it for some reason. + function onMessageReceived(address _originAddress, uint32 _originNetwork, bytes memory _data) external payable { + + require(msg.sender == l2Bridge, "not the expected bridge"); + require(_originAddress == l1GlobalChainInfoPublisher, "only publisher can call us"); + require(_originNetwork == L1_NETWORK_ID, "Bad origin network"); + + (uint64 chainId, address forkonomicToken, uint256 forkFee, bool isL1, address forker, bytes32 questionId, bytes32 result) = abi.decode(_data, (uint64, address, uint256, bool, address, bytes32, bytes32)); + + chainInfo[chainId] = ChainInfo(forkonomicToken, forkFee); + + questionToChainID[isL1][forker][questionId] = chainId; + forkQuestionResults[isL1][forker][questionId] = result; + + } + +} diff --git a/contracts/L2ForkArbitrator.sol b/contracts/L2ForkArbitrator.sol new file mode 100644 index 00000000..ec3afbc7 --- /dev/null +++ b/contracts/L2ForkArbitrator.sol @@ -0,0 +1,231 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +// Allow mixed-case variables for compatibility with reality.eth, eg it uses question_id not questionId +/* solhint-disable var-name-mixedcase */ + +import {L2ChainInfo} from "./L2ChainInfo.sol"; +import {L1GlobalForkRequester} from "./L1GlobalForkRequester.sol"; +import {IRealityETH} from "./interfaces/IRealityETH.sol"; +import {CalculateMoneyBoxAddress} from "./lib/CalculateMoneyBoxAddress.sol"; + +import {IPolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMBridge.sol"; +import {IBridgeMessageReceiver} from "@RealityETH/zkevm-contracts/contracts/interfaces/IBridgeMessageReceiver.sol"; + +/* +This contract is the arbitrator used by governance propositions for AdjudicationFramework contracts. +It charges a dispute fee of 5% of total supply [TODO], which it forwards to L1 when requesting a fork. +If there is already a dispute in progress (ie another fork has been requested but not triggered or we are in the 1 week period before a fork) the new one will be queued. +*/ + +// NB This doesn't implement IArbitrator because that requires slightly more functions than we need +// TODO: Would be good to make a stripped-down IArbitrator that only has the essential functions +contract L2ForkArbitrator is IBridgeMessageReceiver { + + bool public isForkInProgress; + IRealityETH public realitio; + + enum RequestStatus { + NONE, + QUEUED, // We got our payment and put the reality.eth process on hold, but haven't requested initialization yet + FORK_REQUESTED, // Fork request set to L1, result unknown so far + FORK_REQUEST_FAILED // Fork request failed, eg another process was forking + } + + struct ArbitrationRequest { + RequestStatus status; + address payable payer; + uint256 paid; + bytes32 result; + } + + event LogRequestArbitration( + bytes32 indexed question_id, + uint256 fee_paid, + address requester, + uint256 remaining + ); + + mapping(bytes32 => ArbitrationRequest) public arbitrationRequests; + mapping(address => uint256) public refundsDue; + + L2ChainInfo public chainInfo; + L1GlobalForkRequester public l1GlobalForkRequester; + + uint256 public disputeFee; // Normally dispute fee should generally only go down in a fork + + constructor(IRealityETH _realitio, L2ChainInfo _chainInfo, L1GlobalForkRequester _l1GlobalForkRequester, uint256 _initialDisputeFee) { + realitio = _realitio; + chainInfo = _chainInfo; + l1GlobalForkRequester = _l1GlobalForkRequester; + disputeFee = _initialDisputeFee; + } + + /// @notice Return the dispute fee for the specified question. 0 indicates that we won't arbitrate it. + /// @dev Uses a general default, takes a question id param for other contracts that may want to set it per-question. + function getDisputeFee(bytes32) public view returns (uint256) { + return disputeFee; + } + + /// @notice Request arbitration, freezing the question until we send submitAnswerByArbitrator + /// @dev The bounty can be paid only in part, in which case the last person to pay will be considered the payer + /// 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, + "fee must be positive" + ); + + require(arbitrationRequests[question_id].status == RequestStatus.NONE, "Already requested"); + + arbitrationRequests[question_id] = ArbitrationRequest(RequestStatus.QUEUED, payable(msg.sender), msg.value, bytes32(0)); + + realitio.notifyOfArbitrationRequest( + question_id, + msg.sender, + max_previous + ); + emit LogRequestArbitration(question_id, msg.value, msg.sender, 0); + + if (!isForkInProgress) { + requestActivateFork(question_id); + } + + return true; + } + + /// @notice Request a fork via the bridge + /// @dev Talks to the L1 ForkingManager asynchronously, and may fail. + /// @param question_id The question in question + function requestActivateFork( + bytes32 question_id + ) public { + + require(!isForkInProgress, "Already forking"); // Forking over something else + + RequestStatus status = arbitrationRequests[question_id].status; + require(status == RequestStatus.QUEUED || status == RequestStatus.FORK_REQUEST_FAILED, "not awaiting activation"); + arbitrationRequests[question_id].status = RequestStatus.FORK_REQUESTED; + + uint256 forkFee = chainInfo.getForkFee(); + uint256 paid = arbitrationRequests[question_id].paid; + require(paid >= forkFee, "fee paid too low"); + + address l2Bridge = chainInfo.l2Bridge(); + require(l2Bridge != address(0), "l2Bridge not set"); + + IPolygonZkEVMBridge bridge = IPolygonZkEVMBridge(l2Bridge); + + address forkonomicToken = chainInfo.getForkonomicToken(); + + // The receiving contract may get different payments from different requests + // To differentiate our payment, we will use a dedicated MoneyBox contract controlled by l1GlobalForkRequester + // The L1GlobalForkRequester will deploy this as and when it's needed. + // TODO: For now we assume only 1 request is in-flight at a time. If there might be more, differentiate them in the salt. + bytes32 salt = keccak256(abi.encodePacked(address(this), question_id)); + address moneyBox = CalculateMoneyBoxAddress._calculateMoneyBoxAddress(address(l1GlobalForkRequester), salt, address(forkonomicToken)); + + bytes memory permitData; + bridge.bridgeAsset{value: forkFee}( + uint32(0), + moneyBox, + forkFee, // TODO: Should this be 0 since we already sent the forkFee as msg.value? + address(0), // Empty address for the native token + true, + permitData + ); + + isForkInProgress = true; + } + + // If the fork request fails, we will get a message back through the bridge telling us about it + // We will set FORK_REQUEST_FAILED which will allow anyone to request cancellation + function onMessageReceived(address _originAddress, uint32 _originNetwork, bytes memory _data) external payable { + + address l2Bridge = chainInfo.l2Bridge(); + require(msg.sender == l2Bridge, "Not our bridge"); + require(_originNetwork == uint32(0), "Wrong network, WTF"); + require(_originAddress == address(l1GlobalForkRequester), "Unexpected sender"); + + bytes32 question_id = bytes32(_data); + RequestStatus status = arbitrationRequests[question_id].status; + require(status == RequestStatus.FORK_REQUESTED, "not in fork-requested state"); + require(isForkInProgress, "No fork in progress to clear"); + + isForkInProgress = false; + arbitrationRequests[question_id].status = RequestStatus.FORK_REQUEST_FAILED; + + // We don't check the funds are back here, just assume L1GlobalForkRequester send them and they can be recovered. + + } + + /// @notice Submit the arbitrator's answer to a question, assigning the winner automatically. + /// @param question_id The question in question + /// @param last_history_hash The history hash before the final one + /// @param last_answer_or_commitment_id The last answer given, or the commitment ID if it was a commitment. + /// @param last_answerer The address that supplied the last answer + function handleCompletedFork( + bytes32 question_id, + bytes32 last_history_hash, + bytes32 last_answer_or_commitment_id, + address last_answerer + ) external { + + // Read from directory what the result was + RequestStatus status = arbitrationRequests[question_id].status; + require(status == RequestStatus.FORK_REQUESTED, "not in fork-requested state"); + + require(chainInfo.questionToChainID(false, address(this), question_id) > 0, "Dispute not found in ChainInfo"); + + // We get the fork result from the L2ChainInfo contract + bytes32 answer = chainInfo.forkQuestionResults(false, address(this), question_id); + + realitio.assignWinnerAndSubmitAnswerByArbitrator( + question_id, + answer, + arbitrationRequests[question_id].payer, + last_history_hash, + last_answer_or_commitment_id, + last_answerer + ); + + isForkInProgress = false; + delete(arbitrationRequests[question_id]); + + } + + /// @notice Cancel a previous arbitration request + /// @dev This is intended for situations where the stuff is happening non-atomically and the fee changes or someone else forks before us + /// @dev Another way to handle this might be to go back into QUEUED state and let people keep retrying + /// @dev NB This may revert if the contract has returned funds in the bridge but claimAsset hasn't been called yet + /// @param question_id The question in question + function cancelArbitration(bytes32 question_id) external { + + // For simplicity we won't let you cancel until forking is sorted, as you might retry and keep failing for the same reason + require(!isForkInProgress, "Fork in progress"); + + RequestStatus status = arbitrationRequests[question_id].status; + require(status == RequestStatus.FORK_REQUEST_FAILED, "Not in fork-failed state"); + + address payable payer = arbitrationRequests[question_id].payer; + realitio.cancelArbitration(question_id); + + refundsDue[payer] = refundsDue[payer] + arbitrationRequests[question_id].paid; + delete(arbitrationRequests[question_id]); + + } + + function claimRefund() external { + uint256 due = refundsDue[msg.sender]; + refundsDue[msg.sender] = refundsDue[msg.sender] - due; + payable(msg.sender).transfer(due); + } + +} diff --git a/contracts/interfaces/IForkingManager.sol b/contracts/interfaces/IForkingManager.sol index a85be86b..b3889904 100644 --- a/contracts/interfaces/IForkingManager.sol +++ b/contracts/interfaces/IForkingManager.sol @@ -4,11 +4,13 @@ pragma solidity ^0.8.20; import {IForkableStructure} from "./IForkableStructure.sol"; interface IForkingManager is IForkableStructure { + // Dispute contract and call to identify the dispute // that will be used to initiate/justify the fork struct DisputeData { + bool isL1; address disputeContract; - bytes disputeCall; + bytes32 disputeContent; } // Struct containing the addresses of the new implementations @@ -22,6 +24,18 @@ interface IForkingManager is IForkableStructure { uint64 forkID; } + function zkEVM() external returns (address); + function bridge() external returns (address); + function forkonomicToken() external returns (address); + function globalExitRoot() external returns (address); + function arbitrationFee() external returns (uint256); + function disputeData() external returns (bool isL1, address disputeContract, bytes32 disputeContent); + function executionTimeForProposal() external returns (uint256); + + function isForkingInitiated() external returns (bool); + function isForkingExecuted() external returns (bool); + function canFork() external returns (bool); + // Struct that holds an address pair used to store the new child contracts struct AddressPair { address one; @@ -46,4 +60,10 @@ interface IForkingManager is IForkableStructure { uint256 _arbitrationFee, address _chainIdManager ) external; + + function initiateFork( + DisputeData memory _disputeData, + NewImplementations calldata newImplementations + ) external; + } diff --git a/contracts/interfaces/IRealityETH.sol b/contracts/interfaces/IRealityETH.sol index 15245949..c83ad848 100644 --- a/contracts/interfaces/IRealityETH.sol +++ b/contracts/interfaces/IRealityETH.sol @@ -196,6 +196,10 @@ interface IRealityETH is IBalanceHolder { bytes32 question_id ) external view returns (bytes32); + function getEarliestAnswerFromSuppliedHistoryOrRevert( + bytes32 question_id, bytes32[] memory history_hashes, address[] memory addrs, uint256[] memory bonds, bytes32[] memory answers + ) external view returns (bytes32, uint256); + function getFinalAnswer( bytes32 question_id ) external view returns (bytes32); diff --git a/contracts/lib/CalculateMoneyBoxAddress.sol b/contracts/lib/CalculateMoneyBoxAddress.sol new file mode 100644 index 00000000..0dda935e --- /dev/null +++ b/contracts/lib/CalculateMoneyBoxAddress.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +import {MoneyBox} from "../mixin/MoneyBox.sol"; +import {Create2} from "@openzeppelin/contracts/utils/Create2.sol"; + +library CalculateMoneyBoxAddress { + + function _calculateMoneyBoxAddress(address _creator, bytes32 _salt, address _token) internal pure returns (address) { + + return Create2.computeAddress( + _salt, + keccak256(abi.encodePacked( + type(MoneyBox).creationCode, + abi.encode(_token) + )), + _creator + ); + + } + +} diff --git a/contracts/lib/CreateChildren.sol b/contracts/lib/CreateChildren.sol index f02f5caf..516b870a 100644 --- a/contracts/lib/CreateChildren.sol +++ b/contracts/lib/CreateChildren.sol @@ -49,6 +49,9 @@ library CreateChildren { "" ) ); + if (address(implementation) == address(0)) { + implementation = _getImplementation(); + } // Fork 2 can introduce a new implementation, if a different implementation contract // is passed in forkingManager2 = address( diff --git a/contracts/Arbitrator.sol b/contracts/lib/reality-eth/Arbitrator.sol similarity index 97% rename from contracts/Arbitrator.sol rename to contracts/lib/reality-eth/Arbitrator.sol index 11760bd3..48de8249 100644 --- a/contracts/Arbitrator.sol +++ b/contracts/lib/reality-eth/Arbitrator.sol @@ -2,10 +2,10 @@ pragma solidity ^0.8.20; -import "./interfaces/IArbitrator.sol"; -import "./interfaces/IRealityETH.sol"; -import "./interfaces/IERC20.sol"; -import "./mixin/Owned.sol"; +import "./../../interfaces/IArbitrator.sol"; +import "./../../interfaces/IRealityETH.sol"; +import "./../../interfaces/IERC20.sol"; +import "./../../mixin/Owned.sol"; contract Arbitrator is Owned, IArbitrator { IRealityETH public realitio; diff --git a/contracts/mixin/BalanceHolder.sol b/contracts/lib/reality-eth/BalanceHolder.sol similarity index 90% rename from contracts/mixin/BalanceHolder.sol rename to contracts/lib/reality-eth/BalanceHolder.sol index a8a8b268..14bdfb74 100644 --- a/contracts/mixin/BalanceHolder.sol +++ b/contracts/lib/reality-eth/BalanceHolder.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.20; -import "../interfaces/IBalanceHolder.sol"; +import "../../interfaces/IBalanceHolder.sol"; contract BalanceHolder is IBalanceHolder { mapping(address => uint256) public balanceOf; diff --git a/contracts/mixin/BalanceHolder_ERC20.sol b/contracts/lib/reality-eth/BalanceHolder_ERC20.sol similarity index 63% rename from contracts/mixin/BalanceHolder_ERC20.sol rename to contracts/lib/reality-eth/BalanceHolder_ERC20.sol index f9dd01b8..e9a24069 100644 --- a/contracts/mixin/BalanceHolder_ERC20.sol +++ b/contracts/lib/reality-eth/BalanceHolder_ERC20.sol @@ -2,8 +2,10 @@ pragma solidity ^0.8.20; -import "../interfaces/IERC20.sol"; -import "../interfaces/IBalanceHolder_ERC20.sol"; +/* solhint-disable contract-name-camelcase */ + +import {IERC20} from "../../interfaces/IERC20.sol"; +import {IBalanceHolder_ERC20} from "../../interfaces/IBalanceHolder_ERC20.sol"; contract BalanceHolder_ERC20 is IBalanceHolder_ERC20 { IERC20 public token; @@ -15,7 +17,7 @@ contract BalanceHolder_ERC20 is IBalanceHolder_ERC20 { function withdraw() public { uint256 bal = balanceOf[msg.sender]; balanceOf[msg.sender] = 0; - require(token.transfer(msg.sender, bal)); + require(token.transfer(msg.sender, bal), "Transfer failed"); emit LogWithdraw(msg.sender, bal); } } diff --git a/contracts/lib/reality-eth/RealityETH-3.0.sol b/contracts/lib/reality-eth/RealityETH-3.0.sol new file mode 100644 index 00000000..e9778f74 --- /dev/null +++ b/contracts/lib/reality-eth/RealityETH-3.0.sol @@ -0,0 +1,930 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +import './../../interfaces/IRealityETH.sol'; + +import './BalanceHolder.sol'; + +contract RealityETH_v3_0 is BalanceHolder, IRealityETH { + + address constant NULL_ADDRESS = address(0); + + // History hash when no history is created, or history has been cleared + bytes32 constant NULL_HASH = bytes32(0); + + // An unitinalized finalize_ts for a question will indicate an unanswered question. + uint32 constant UNANSWERED = 0; + + // An unanswered reveal_ts for a commitment will indicate that it does not exist. + uint256 constant COMMITMENT_NON_EXISTENT = 0; + + // Commit->reveal timeout is 1/8 of the question timeout (rounded down). + uint32 constant COMMITMENT_TIMEOUT_RATIO = 8; + + // Proportion withheld when you claim an earlier bond. + uint256 constant BOND_CLAIM_FEE_PROPORTION = 40; // One 40th ie 2.5% + + // Special value representing a question that was answered too soon. + // bytes32(-2). By convention we use bytes32(-1) for "invalid", although the contract does not handle this. + bytes32 constant UNRESOLVED_ANSWER = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe; + + struct Question { + bytes32 content_hash; + address arbitrator; + uint32 opening_ts; + uint32 timeout; + uint32 finalize_ts; + bool is_pending_arbitration; + uint256 bounty; + bytes32 best_answer; + bytes32 history_hash; + uint256 bond; + uint256 min_bond; + } + + // Stored in a mapping indexed by commitment_id, a hash of commitment hash, question, bond. + struct Commitment { + uint32 reveal_ts; + bool is_revealed; + bytes32 revealed_answer; + } + + // Only used when claiming more bonds than fits into a transaction + // Stored in a mapping indexed by question_id. + struct Claim { + address payee; + uint256 last_bond; + uint256 queued_funds; + } + + uint256 nextTemplateID = 0; + mapping(uint256 => uint256) public templates; + mapping(uint256 => bytes32) public template_hashes; + mapping(bytes32 => Question) public questions; + mapping(bytes32 => Claim) public question_claims; + mapping(bytes32 => Commitment) public commitments; + mapping(address => uint256) public arbitrator_question_fees; + mapping(bytes32 => bytes32) public reopened_questions; + mapping(bytes32 => bool) public reopener_questions; + + + modifier onlyArbitrator(bytes32 question_id) { + require(msg.sender == questions[question_id].arbitrator, "msg.sender must be arbitrator"); + _; + } + + modifier stateAny() { + _; + } + + modifier stateNotCreated(bytes32 question_id) { + require(questions[question_id].timeout == 0, "question must not exist"); + _; + } + + modifier stateOpen(bytes32 question_id) { + require(questions[question_id].timeout > 0, "question must exist"); + require(!questions[question_id].is_pending_arbitration, "question must not be pending arbitration"); + uint32 finalize_ts = questions[question_id].finalize_ts; + require(finalize_ts == UNANSWERED || finalize_ts > uint32(block.timestamp), "finalization deadline must not have passed"); + uint32 opening_ts = questions[question_id].opening_ts; + require(opening_ts == 0 || opening_ts <= uint32(block.timestamp), "opening date must have passed"); + _; + } + + modifier statePendingArbitration(bytes32 question_id) { + require(questions[question_id].is_pending_arbitration, "question must be pending arbitration"); + _; + } + + modifier stateOpenOrPendingArbitration(bytes32 question_id) { + require(questions[question_id].timeout > 0, "question must exist"); + uint32 finalize_ts = questions[question_id].finalize_ts; + require(finalize_ts == UNANSWERED || finalize_ts > uint32(block.timestamp), "finalization dealine must not have passed"); + uint32 opening_ts = questions[question_id].opening_ts; + require(opening_ts == 0 || opening_ts <= uint32(block.timestamp), "opening date must have passed"); + _; + } + + modifier stateFinalized(bytes32 question_id) { + require(isFinalized(question_id), "question must be finalized"); + _; + } + + modifier bondMustDoubleAndMatchMinimum(bytes32 question_id) { + require(msg.value > 0, "bond must be positive"); + uint256 current_bond = questions[question_id].bond; + if (current_bond == 0) { + require(msg.value >= (questions[question_id].min_bond), "bond must exceed the minimum"); + } else { + require(msg.value >= (current_bond * 2), "bond must be double at least previous bond"); + } + _; + } + + modifier previousBondMustNotBeatMaxPrevious(bytes32 question_id, uint256 max_previous) { + if (max_previous > 0) { + require(questions[question_id].bond <= max_previous, "bond must exceed max_previous"); + } + _; + } + + /// @notice Constructor, sets up some initial templates + /// @dev Creates some generalized templates for different question types used in the DApp. + constructor() { + createTemplate('{"title": "%s", "type": "bool", "category": "%s", "lang": "%s"}'); + createTemplate('{"title": "%s", "type": "uint", "decimals": 18, "category": "%s", "lang": "%s"}'); + createTemplate('{"title": "%s", "type": "single-select", "outcomes": [%s], "category": "%s", "lang": "%s"}'); + createTemplate('{"title": "%s", "type": "multiple-select", "outcomes": [%s], "category": "%s", "lang": "%s"}'); + createTemplate('{"title": "%s", "type": "datetime", "category": "%s", "lang": "%s"}'); + } + + /// @notice Function for arbitrator to set an optional per-question fee. + /// @dev The per-question fee, charged when a question is asked, is intended as an anti-spam measure. + /// @param fee The fee to be charged by the arbitrator when a question is asked + function setQuestionFee(uint256 fee) + stateAny() + external { + arbitrator_question_fees[msg.sender] = fee; + emit LogSetQuestionFee(msg.sender, fee); + } + + /// @notice Create a reusable template, which should be a JSON document. + /// Placeholders should use gettext() syntax, eg %s. + /// @dev Template data is only stored in the event logs, but its block number is kept in contract storage. + /// @param content The template content + /// @return The ID of the newly-created template, which is created sequentially. + function createTemplate(string memory content) + stateAny() + public returns (uint256) { + uint256 id = nextTemplateID; + templates[id] = block.number; + template_hashes[id] = keccak256(abi.encodePacked(content)); + emit LogNewTemplate(id, msg.sender, content); + nextTemplateID = id + 1; + return id; + } + + /// @notice Create a new reusable template and use it to ask a question + /// @dev Template data is only stored in the event logs, but its block number is kept in contract storage. + /// @param content The template content + /// @param question A string containing the parameters that will be passed into the template to make the question + /// @param arbitrator The arbitration contract that will have the final word on the answer if there is a dispute + /// @param timeout How long the contract should wait after the answer is changed before finalizing on that answer + /// @param opening_ts If set, the earliest time it should be possible to answer the question. + /// @param nonce A user-specified nonce used in the question ID. Change it to repeat a question. + /// @return The ID of the newly-created template, which is created sequentially. + function createTemplateAndAskQuestion( + string memory content, + string memory question, address arbitrator, uint32 timeout, uint32 opening_ts, uint256 nonce + ) + // stateNotCreated is enforced by the internal _askQuestion + public payable returns (bytes32) { + uint256 template_id = createTemplate(content); + return askQuestion(template_id, question, arbitrator, timeout, opening_ts, nonce); + } + + /// @notice Ask a new question and return the ID + /// @dev Template data is only stored in the event logs, but its block number is kept in contract storage. + /// @param template_id The ID number of the template the question will use + /// @param question A string containing the parameters that will be passed into the template to make the question + /// @param arbitrator The arbitration contract that will have the final word on the answer if there is a dispute + /// @param timeout How long the contract should wait after the answer is changed before finalizing on that answer + /// @param opening_ts If set, the earliest time it should be possible to answer the question. + /// @param nonce A user-specified nonce used in the question ID. Change it to repeat a question. + /// @return The ID of the newly-created question, created deterministically. + function askQuestion(uint256 template_id, string memory question, address arbitrator, uint32 timeout, uint32 opening_ts, uint256 nonce) + // stateNotCreated is enforced by the internal _askQuestion + public payable returns (bytes32) { + + require(templates[template_id] > 0, "template must exist"); + + bytes32 content_hash = keccak256(abi.encodePacked(template_id, opening_ts, question)); + bytes32 question_id = keccak256(abi.encodePacked(content_hash, arbitrator, timeout, uint256(0), address(this), msg.sender, nonce)); + + // We emit this event here because _askQuestion doesn't need to know the unhashed question. Other events are emitted by _askQuestion. + emit LogNewQuestion(question_id, msg.sender, template_id, question, content_hash, arbitrator, timeout, opening_ts, nonce, block.timestamp); + _askQuestion(question_id, content_hash, arbitrator, timeout, opening_ts, 0); + + return question_id; + } + + /// @notice Ask a new question and return the ID + /// @dev Template data is only stored in the event logs, but its block number is kept in contract storage. + /// @param template_id The ID number of the template the question will use + /// @param question A string containing the parameters that will be passed into the template to make the question + /// @param arbitrator The arbitration contract that will have the final word on the answer if there is a dispute + /// @param timeout How long the contract should wait after the answer is changed before finalizing on that answer + /// @param opening_ts If set, the earliest time it should be possible to answer the question. + /// @param nonce A user-specified nonce used in the question ID. Change it to repeat a question. + /// @param min_bond The minimum bond that may be used for an answer. + /// @return The ID of the newly-created question, created deterministically. + function askQuestionWithMinBond(uint256 template_id, string memory question, address arbitrator, uint32 timeout, uint32 opening_ts, uint256 nonce, uint256 min_bond) + // stateNotCreated is enforced by the internal _askQuestion + public payable returns (bytes32) { + + require(templates[template_id] > 0, "template must exist"); + + bytes32 content_hash = keccak256(abi.encodePacked(template_id, opening_ts, question)); + bytes32 question_id = keccak256(abi.encodePacked(content_hash, arbitrator, timeout, min_bond, address(this), msg.sender, nonce)); + + // We emit this event here because _askQuestion doesn't need to know the unhashed question. + // Other events are emitted by _askQuestion. + emit LogNewQuestion(question_id, msg.sender, template_id, question, content_hash, arbitrator, timeout, opening_ts, nonce, block.timestamp); + _askQuestion(question_id, content_hash, arbitrator, timeout, opening_ts, min_bond); + + return question_id; + } + + function _askQuestion(bytes32 question_id, bytes32 content_hash, address arbitrator, uint32 timeout, uint32 opening_ts, uint256 min_bond) + stateNotCreated(question_id) + internal { + + // A timeout of 0 makes no sense, and we will use this to check existence + require(timeout > 0, "timeout must be positive"); + require(timeout < 365 days, "timeout must be less than 365 days"); + + uint256 bounty = msg.value; + + // The arbitrator can set a fee for asking a question. + // This is intended as an anti-spam defence. + // The fee is waived if the arbitrator is asking the question. + // This allows them to set an impossibly high fee and make users proxy the question through them. + // This would allow more sophisticated pricing, question whitelisting etc. + if (arbitrator != NULL_ADDRESS && msg.sender != arbitrator) { + uint256 question_fee = arbitrator_question_fees[arbitrator]; + require(bounty >= question_fee, "ETH provided must cover question fee"); + bounty = bounty - question_fee; + balanceOf[arbitrator] = balanceOf[arbitrator] + question_fee; + } + + questions[question_id].content_hash = content_hash; + questions[question_id].arbitrator = arbitrator; + questions[question_id].opening_ts = opening_ts; + questions[question_id].timeout = timeout; + + if (bounty > 0) { + questions[question_id].bounty = bounty; + emit LogFundAnswerBounty(question_id, bounty, bounty, msg.sender); + } + + if (min_bond > 0) { + questions[question_id].min_bond = min_bond; + emit LogMinimumBond(question_id, min_bond); + } + + } + + /// @notice Add funds to the bounty for a question + /// @dev Add bounty funds after the initial question creation. Can be done any time until the question is finalized. + /// @param question_id The ID of the question you wish to fund + function fundAnswerBounty(bytes32 question_id) + stateOpen(question_id) + external payable { + questions[question_id].bounty = questions[question_id].bounty + msg.value; + emit LogFundAnswerBounty(question_id, msg.value, questions[question_id].bounty, msg.sender); + } + + /// @notice Submit an answer for a question. + /// @dev Adds the answer to the history and updates the current "best" answer. + /// May be subject to front-running attacks; Substitute submitAnswerCommitment()->submitAnswerReveal() to prevent them. + /// @param question_id The ID of the question + /// @param answer The answer, encoded into bytes32 + /// @param max_previous If specified, reverts if a bond higher than this was submitted after you sent your transaction. + function submitAnswer(bytes32 question_id, bytes32 answer, uint256 max_previous) + stateOpen(question_id) + bondMustDoubleAndMatchMinimum(question_id) + previousBondMustNotBeatMaxPrevious(question_id, max_previous) + external payable { + _addAnswerToHistory(question_id, answer, msg.sender, msg.value, false); + _updateCurrentAnswer(question_id, answer); + } + + /// @notice Submit an answer for a question, crediting it to the specified account. + /// @dev Adds the answer to the history and updates the current "best" answer. + /// May be subject to front-running attacks; Substitute submitAnswerCommitment()->submitAnswerReveal() to prevent them. + /// @param question_id The ID of the question + /// @param answer The answer, encoded into bytes32 + /// @param max_previous If specified, reverts if a bond higher than this was submitted after you sent your transaction. + /// @param answerer The account to which the answer should be credited + function submitAnswerFor(bytes32 question_id, bytes32 answer, uint256 max_previous, address answerer) + stateOpen(question_id) + bondMustDoubleAndMatchMinimum(question_id) + previousBondMustNotBeatMaxPrevious(question_id, max_previous) + external payable { + require(answerer != NULL_ADDRESS, "answerer must be non-zero"); + _addAnswerToHistory(question_id, answer, answerer, msg.value, false); + _updateCurrentAnswer(question_id, answer); + } + + // @notice Verify and store a commitment, including an appropriate timeout + // @param question_id The ID of the question to store + // @param commitment The ID of the commitment + function _storeCommitment(bytes32 question_id, bytes32 commitment_id) + internal + { + require(commitments[commitment_id].reveal_ts == COMMITMENT_NON_EXISTENT, "commitment must not already exist"); + + uint32 commitment_timeout = questions[question_id].timeout / COMMITMENT_TIMEOUT_RATIO; + commitments[commitment_id].reveal_ts = uint32(block.timestamp) + commitment_timeout; + } + + /// @notice Submit the hash of an answer, laying your claim to that answer if you reveal it in a subsequent transaction. + /// @dev Creates a hash, commitment_id, uniquely identifying this answer, to this question, with this bond. + /// The commitment_id is stored in the answer history where the answer would normally go. + /// Does not update the current best answer - this is left to the later submitAnswerReveal() transaction. + /// @param question_id The ID of the question + /// @param answer_hash The hash of your answer, plus a nonce that you will later reveal + /// @param max_previous If specified, reverts if a bond higher than this was submitted after you sent your transaction. + /// @param _answerer If specified, the address to be given as the question answerer. Defaults to the sender. + /// @dev Specifying the answerer is useful if you want to delegate the commit-and-reveal to a third-party. + function submitAnswerCommitment(bytes32 question_id, bytes32 answer_hash, uint256 max_previous, address _answerer) + stateOpen(question_id) + bondMustDoubleAndMatchMinimum(question_id) + previousBondMustNotBeatMaxPrevious(question_id, max_previous) + external payable { + + bytes32 commitment_id = keccak256(abi.encodePacked(question_id, answer_hash, msg.value)); + address answerer = (_answerer == NULL_ADDRESS) ? msg.sender : _answerer; + _storeCommitment(question_id, commitment_id); + _addAnswerToHistory(question_id, commitment_id, answerer, msg.value, true); + + } + + /// @notice Submit the answer whose hash you sent in a previous submitAnswerCommitment() transaction + /// @dev Checks the parameters supplied recreate an existing commitment, and stores the revealed answer + /// Updates the current answer unless someone has since supplied a new answer with a higher bond + /// msg.sender is intentionally not restricted to the user who originally sent the commitment; + /// For example, the user may want to provide the answer+nonce to a third-party service and let them send the tx + /// NB If we are pending arbitration, it will be up to the arbitrator to wait and see any outstanding reveal is sent + /// @param question_id The ID of the question + /// @param answer The answer, encoded as bytes32 + /// @param nonce The nonce that, combined with the answer, recreates the answer_hash you gave in submitAnswerCommitment() + /// @param bond The bond that you paid in your submitAnswerCommitment() transaction + function submitAnswerReveal(bytes32 question_id, bytes32 answer, uint256 nonce, uint256 bond) + stateOpenOrPendingArbitration(question_id) + external { + + bytes32 answer_hash = keccak256(abi.encodePacked(answer, nonce)); + bytes32 commitment_id = keccak256(abi.encodePacked(question_id, answer_hash, bond)); + + require(!commitments[commitment_id].is_revealed, "commitment must not have been revealed yet"); + require(commitments[commitment_id].reveal_ts > uint32(block.timestamp), "reveal deadline must not have passed"); + + commitments[commitment_id].revealed_answer = answer; + commitments[commitment_id].is_revealed = true; + + if (bond == questions[question_id].bond) { + _updateCurrentAnswer(question_id, answer); + } + + emit LogAnswerReveal(question_id, msg.sender, answer_hash, answer, nonce, bond); + + } + + function _addAnswerToHistory(bytes32 question_id, bytes32 answer_or_commitment_id, address answerer, uint256 bond, bool is_commitment) + internal + { + bytes32 new_history_hash = keccak256(abi.encodePacked(questions[question_id].history_hash, answer_or_commitment_id, bond, answerer, is_commitment)); + + // Update the current bond level, if there's a bond (ie anything except arbitration) + if (bond > 0) { + questions[question_id].bond = bond; + } + questions[question_id].history_hash = new_history_hash; + + emit LogNewAnswer(answer_or_commitment_id, question_id, new_history_hash, answerer, bond, block.timestamp, is_commitment); + } + + function _updateCurrentAnswer(bytes32 question_id, bytes32 answer) + internal { + questions[question_id].best_answer = answer; + questions[question_id].finalize_ts = uint32(block.timestamp) + questions[question_id].timeout; + } + + // Like _updateCurrentAnswer but without advancing the timeout + function _updateCurrentAnswerByArbitrator(bytes32 question_id, bytes32 answer) + internal { + questions[question_id].best_answer = answer; + questions[question_id].finalize_ts = uint32(block.timestamp); + } + + /// @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 + /// @param max_previous If specified, reverts if a bond higher than this was submitted after you sent your transaction. + function notifyOfArbitrationRequest(bytes32 question_id, address requester, uint256 max_previous) + onlyArbitrator(question_id) + stateOpen(question_id) + previousBondMustNotBeatMaxPrevious(question_id, max_previous) + external { + require(questions[question_id].finalize_ts > UNANSWERED, "Question must already have an answer when arbitration is requested"); + questions[question_id].is_pending_arbitration = true; + emit LogNotifyOfArbitrationRequest(question_id, requester); + } + + /// @notice Cancel a previously-requested arbitration and extend the timeout + /// @dev Useful when doing arbitration across chains that can't be requested atomically + /// @param question_id The ID of the question + function cancelArbitration(bytes32 question_id) + onlyArbitrator(question_id) + statePendingArbitration(question_id) + external { + questions[question_id].is_pending_arbitration = false; + questions[question_id].finalize_ts = uint32(block.timestamp) + questions[question_id].timeout; + emit LogCancelArbitration(question_id); + } + + /// @notice Submit the answer for a question, for use by the arbitrator. + /// @dev Doesn't require (or allow) a bond. + /// If the current final answer is correct, the account should be whoever submitted it. + /// If the current final answer is wrong, the account should be whoever paid for arbitration. + /// However, the answerer stipulations are not enforced by the contract. + /// @param question_id The ID of the question + /// @param answer The answer, encoded into bytes32 + /// @param answerer The account credited with this answer for the purpose of bond claims + function submitAnswerByArbitrator(bytes32 question_id, bytes32 answer, address answerer) + onlyArbitrator(question_id) + statePendingArbitration(question_id) + public { + + require(answerer != NULL_ADDRESS, "answerer must be provided"); + emit LogFinalize(question_id, answer); + + questions[question_id].is_pending_arbitration = false; + _addAnswerToHistory(question_id, answer, answerer, 0, false); + _updateCurrentAnswerByArbitrator(question_id, answer); + + } + + /// @notice Submit the answer for a question, for use by the arbitrator, working out the appropriate winner based on the last answer details. + /// @dev Doesn't require (or allow) a bond. + /// @param question_id The ID of the question + /// @param answer The answer, encoded into bytes32 + /// @param payee_if_wrong The account to by credited as winner if the last answer given is wrong, usually the account that paid the arbitrator + /// @param last_history_hash The history hash before the final one + /// @param last_answer_or_commitment_id The last answer given, or the commitment ID if it was a commitment. + /// @param last_answerer The address that supplied the last answer + function assignWinnerAndSubmitAnswerByArbitrator(bytes32 question_id, bytes32 answer, address payee_if_wrong, bytes32 last_history_hash, bytes32 last_answer_or_commitment_id, address last_answerer) + external { + bool is_commitment = _verifyHistoryInputOrRevert(questions[question_id].history_hash, last_history_hash, last_answer_or_commitment_id, questions[question_id].bond, last_answerer); + + address payee; + // If the last answer is an unrevealed commit, it's always wrong. + // For anything else, the last answer was set as the "best answer" in submitAnswer or submitAnswerReveal. + if (is_commitment && !commitments[last_answer_or_commitment_id].is_revealed) { + require(commitments[last_answer_or_commitment_id].reveal_ts < uint32(block.timestamp), "You must wait for the reveal deadline before finalizing"); + payee = payee_if_wrong; + } else { + payee = (questions[question_id].best_answer == answer) ? last_answerer : payee_if_wrong; + } + submitAnswerByArbitrator(question_id, answer, payee); + } + + + /// @notice Report whether the answer to the specified question is finalized + /// @param question_id The ID of the question + /// @return Return true if finalized + function isFinalized(bytes32 question_id) + view public returns (bool) { + uint32 finalize_ts = questions[question_id].finalize_ts; + return ( !questions[question_id].is_pending_arbitration && (finalize_ts > UNANSWERED) && (finalize_ts <= uint32(block.timestamp)) ); + } + + /// @notice (Deprecated) Return the final answer to the specified question, or revert if there isn't one + /// @param question_id The ID of the question + /// @return The answer formatted as a bytes32 + function getFinalAnswer(bytes32 question_id) + stateFinalized(question_id) + external view returns (bytes32) { + return questions[question_id].best_answer; + } + + /// @notice Return the final answer to the specified question, or revert if there isn't one + /// @param question_id The ID of the question + /// @return The answer formatted as a bytes32 + function resultFor(bytes32 question_id) + stateFinalized(question_id) + public view returns (bytes32) { + return questions[question_id].best_answer; + } + + /// @notice Returns whether the question was answered before it had an answer, ie resolved to UNRESOLVED_ANSWER + /// @param question_id The ID of the question + function isSettledTooSoon(bytes32 question_id) + public view returns(bool) { + return (resultFor(question_id) == UNRESOLVED_ANSWER); + } + + /// @notice Like resultFor(), but errors out if settled too soon, or returns the result of a replacement if it was reopened at the right time and settled + /// @param question_id The ID of the question + function resultForOnceSettled(bytes32 question_id) + external view returns(bytes32) { + bytes32 result = resultFor(question_id); + if (result == UNRESOLVED_ANSWER) { + // Try the replacement + bytes32 replacement_id = reopened_questions[question_id]; + require(replacement_id != bytes32(0x0), "Question was settled too soon and has not been reopened"); + // We only try one layer down rather than recursing to keep the gas costs predictable + result = resultFor(replacement_id); + require(result != UNRESOLVED_ANSWER, "Question replacement was settled too soon and has not been reopened"); + } + return result; + } + + /// @notice Asks a new question reopening a previously-asked question that was settled too soon + /// @dev A special version of askQuestion() that replaces a previous question that was settled too soon + /// @param template_id The ID number of the template the question will use + /// @param question A string containing the parameters that will be passed into the template to make the question + /// @param arbitrator The arbitration contract that will have the final word on the answer if there is a dispute + /// @param timeout How long the contract should wait after the answer is changed before finalizing on that answer + /// @param opening_ts If set, the earliest time it should be possible to answer the question. + /// @param nonce A user-specified nonce used in the question ID. Change it to repeat a question. + /// @param min_bond The minimum bond that can be used to provide the first answer. + /// @param reopens_question_id The ID of the question this reopens + /// @return The ID of the newly-created question, created deterministically. + function reopenQuestion(uint256 template_id, string memory question, address arbitrator, uint32 timeout, uint32 opening_ts, uint256 nonce, uint256 min_bond, bytes32 reopens_question_id) + // stateNotCreated is enforced by the internal _askQuestion + public payable returns (bytes32) { + + require(isSettledTooSoon(reopens_question_id), "You can only reopen questions that resolved as settled too soon"); + + bytes32 content_hash = keccak256(abi.encodePacked(template_id, opening_ts, question)); + + // A reopening must exactly match the original question, except for the nonce and the creator + require(content_hash == questions[reopens_question_id].content_hash, "content hash mismatch"); + require(arbitrator == questions[reopens_question_id].arbitrator, "arbitrator mismatch"); + require(timeout == questions[reopens_question_id].timeout, "timeout mismatch"); + require(opening_ts == questions[reopens_question_id].opening_ts , "opening_ts mismatch"); + require(min_bond == questions[reopens_question_id].min_bond, "min_bond mismatch"); + + // If the the question was itself reopening some previous question, you'll have to re-reopen the previous question first. + // This ensures the bounty can be passed on to the next attempt of the original question. + require(!reopener_questions[reopens_question_id], "Question is already reopening a previous question"); + + // A question can only be reopened once, unless the reopening was also settled too soon in which case it can be replaced + bytes32 existing_reopen_question_id = reopened_questions[reopens_question_id]; + + // Normally when we reopen a question we will take its bounty and pass it on to the reopened version. + bytes32 take_bounty_from_question_id = reopens_question_id; + // If the question has already been reopened but was again settled too soon, we can transfer its bounty to the next attempt. + if (existing_reopen_question_id != bytes32(0)) { + require(isSettledTooSoon(existing_reopen_question_id), "Question has already been reopened"); + // We'll overwrite the reopening with our new question and move the bounty. + // Once that's done we'll detach the failed reopener and you'll be able to reopen that too if you really want, but without the bounty. + reopener_questions[existing_reopen_question_id] = false; + take_bounty_from_question_id = existing_reopen_question_id; + } + + bytes32 question_id = askQuestionWithMinBond(template_id, question, arbitrator, timeout, opening_ts, nonce, min_bond); + + reopened_questions[reopens_question_id] = question_id; + reopener_questions[question_id] = true; + + questions[question_id].bounty = questions[take_bounty_from_question_id].bounty + questions[question_id].bounty; + questions[take_bounty_from_question_id].bounty = 0; + + emit LogReopenQuestion(question_id, reopens_question_id); + + return question_id; + } + + /// @notice Return the final answer to the specified question, provided it matches the specified criteria. + /// @dev Reverts if the question is not finalized, or if it does not match the specified criteria. + /// @param question_id The ID of the question + /// @param content_hash The hash of the question content (template ID + opening time + question parameter string) + /// @param arbitrator The arbitrator chosen for the question (regardless of whether they are asked to arbitrate) + /// @param min_timeout The timeout set in the initial question settings must be this high or higher + /// @param min_bond The bond sent with the final answer must be this high or higher + /// @return The answer formatted as a bytes32 + function getFinalAnswerIfMatches( + bytes32 question_id, + bytes32 content_hash, address arbitrator, uint32 min_timeout, uint256 min_bond + ) + stateFinalized(question_id) + external view returns (bytes32) { + require(content_hash == questions[question_id].content_hash, "content hash must match"); + require(arbitrator == questions[question_id].arbitrator, "arbitrator must match"); + require(min_timeout <= questions[question_id].timeout, "timeout must be long enough"); + require(min_bond <= questions[question_id].bond, "bond must be high enough"); + return questions[question_id].best_answer; + } + + /// @notice Assigns the winnings (bounty and bonds) to everyone who gave the accepted answer + /// Caller must provide the answer history, in reverse order + /// @dev Works up the chain and assign bonds to the person who gave the right answer + /// If someone gave the winning answer earlier, they must get paid from the higher bond + /// That means we can't pay out the bond added at n until we have looked at n-1 + /// The first answer is authenticated by checking against the stored history_hash. + /// One of the inputs to history_hash is the history_hash before it, so we use that to authenticate the next entry, etc + /// Once we get to a null hash we'll know we're done and there are no more answers. + /// Usually you would call the whole thing in a single transaction, but if not then the data is persisted to pick up later. + /// @param question_id The ID of the question + /// @param history_hashes Second-last-to-first, the hash of each history entry. (Final one should be empty). + /// @param addrs Last-to-first, the address of each answerer or commitment sender + /// @param bonds Last-to-first, the bond supplied with each answer or commitment + /// @param answers Last-to-first, each answer supplied, or commitment ID if the answer was supplied with commit->reveal + function claimWinnings( + bytes32 question_id, + bytes32[] memory history_hashes, address[] memory addrs, uint256[] memory bonds, bytes32[] memory answers + ) + stateFinalized(question_id) + public { + + require(history_hashes.length > 0, "at least one history hash entry must be provided"); + + // These are only set if we split our claim over multiple transactions. + address payee = question_claims[question_id].payee; + uint256 last_bond = question_claims[question_id].last_bond; + uint256 queued_funds = question_claims[question_id].queued_funds; + + // Starts as the hash of the final answer submitted. It'll be cleared when we're done. + // If we're splitting the claim over multiple transactions, it'll be the hash where we left off last time + bytes32 last_history_hash = questions[question_id].history_hash; + + bytes32 best_answer = questions[question_id].best_answer; + + uint256 i; + for (i = 0; i < history_hashes.length; i++) { + + // Check input against the history hash, and see which of 2 possible values of is_commitment fits. + bool is_commitment = _verifyHistoryInputOrRevert(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i]); + + queued_funds = queued_funds + last_bond; + (queued_funds, payee) = _processHistoryItem( + question_id, best_answer, queued_funds, payee, + addrs[i], bonds[i], answers[i], is_commitment); + + // Line the bond up for next time, when it will be added to somebody's queued_funds + last_bond = bonds[i]; + + // Burn (just leave in contract balance) a fraction of all bonds except the final one. + // This creates a cost to increasing your own bond, which could be used to delay resolution maliciously + if (last_bond != questions[question_id].bond) { + last_bond = last_bond - last_bond / BOND_CLAIM_FEE_PROPORTION; + } + + last_history_hash = history_hashes[i]; + + } + + if (last_history_hash != NULL_HASH) { + // We haven't yet got to the null hash (1st answer), ie the caller didn't supply the full answer chain. + // Persist the details so we can pick up later where we left off later. + + // If we know who to pay we can go ahead and pay them out, only keeping back last_bond + // (We always know who to pay unless all we saw were unrevealed commits) + if (payee != NULL_ADDRESS) { + _payPayee(question_id, payee, queued_funds); + queued_funds = 0; + } + + question_claims[question_id].payee = payee; + question_claims[question_id].last_bond = last_bond; + question_claims[question_id].queued_funds = queued_funds; + } else { + // There is nothing left below us so the payee can keep what remains + _payPayee(question_id, payee, queued_funds + last_bond); + delete question_claims[question_id]; + } + + questions[question_id].history_hash = last_history_hash; + + } + + function _payPayee(bytes32 question_id, address payee, uint256 value) + internal { + balanceOf[payee] = balanceOf[payee] + value; + emit LogClaim(question_id, payee, value); + } + + /// @notice Returns the value of the earliest answer in the supplied history + /// Caller must provide the answer history, in reverse order back to the item they want to check + /// @dev Not necessarily the entire history + /// @param question_id The ID of the question + /// @param history_hashes Second-last-to-first, the hash of each history entry. (Final one should be empty). + /// @param addrs Last-to-first, the address of each answerer or commitment sender + /// @param bonds Last-to-first, the bond supplied with each answer or commitment + /// @param answers Last-to-first, each answer supplied, or commitment ID if the answer was supplied with commit->reveal + function getEarliestAnswerFromSuppliedHistoryOrRevert( + bytes32 question_id, + bytes32[] memory history_hashes, address[] memory addrs, uint256[] memory bonds, bytes32[] memory answers + ) + stateAny() + external view returns (bytes32, uint256) { + // Go through the history reverting if any of it is wrong + bool is_commitment; + bytes32 last_history_hash = questions[question_id].history_hash; + uint256 i; + for (i = 0; i < history_hashes.length; i++) { + is_commitment = _verifyHistoryInputOrRevert(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i]); + last_history_hash = history_hashes[i]; + } + + // When we get to the final answer, unwrap the commitment if required and return + uint256 last_i = history_hashes.length-1; + bytes32 answer_or_commitment_id = answers[last_i]; + bytes32 answer; + if (is_commitment) { + require(commitments[answer_or_commitment_id].is_revealed, "Earliest answer is an unrevealed commitment"); + answer = commitments[answer_or_commitment_id].revealed_answer; + } else { + answer = answer_or_commitment_id; + } + return (answer, bonds[last_i]); + } + + function _verifyHistoryInputOrRevert( + bytes32 last_history_hash, + bytes32 history_hash, bytes32 answer, uint256 bond, address addr + ) + internal pure returns (bool) { + if (last_history_hash == keccak256(abi.encodePacked(history_hash, answer, bond, addr, true)) ) { + return true; + } + if (last_history_hash == keccak256(abi.encodePacked(history_hash, answer, bond, addr, false)) ) { + return false; + } + revert("History input provided did not match the expected hash"); + } + + function _processHistoryItem( + bytes32 question_id, bytes32 best_answer, + uint256 queued_funds, address payee, + address addr, uint256 bond, bytes32 answer, bool is_commitment + ) + internal returns (uint256, address) { + + // For commit-and-reveal, the answer history holds the commitment ID instead of the answer. + // We look at the referenced commitment ID and switch in the actual answer. + if (is_commitment) { + bytes32 commitment_id = answer; + // If it's a commit but it hasn't been revealed, it will always be considered wrong. + if (!commitments[commitment_id].is_revealed) { + delete commitments[commitment_id]; + return (queued_funds, payee); + } else { + answer = commitments[commitment_id].revealed_answer; + delete commitments[commitment_id]; + } + } + + if (answer == best_answer) { + + if (payee == NULL_ADDRESS) { + + // The entry is for the first payee we come to, ie the winner. + // They get the question bounty. + payee = addr; + + if (best_answer != UNRESOLVED_ANSWER && questions[question_id].bounty > 0) { + _payPayee(question_id, payee, questions[question_id].bounty); + questions[question_id].bounty = 0; + } + + } else if (addr != payee) { + + // Answerer has changed, ie we found someone lower down who needs to be paid + + // The lower answerer will take over receiving bonds from higher answerer. + // They should also be paid the takeover fee, which is set at a rate equivalent to their bond. + // (This is our arbitrary rule, to give consistent right-answerers a defence against high-rollers.) + + // There should be enough for the fee, but if not, take what we have. + // There's an edge case involving weird arbitrator behaviour where we may be short. + uint256 answer_takeover_fee = (queued_funds >= bond) ? bond : queued_funds; + // Settle up with the old (higher-bonded) payee + _payPayee(question_id, payee, queued_funds - answer_takeover_fee); + + // Now start queued_funds again for the new (lower-bonded) payee + payee = addr; + queued_funds = answer_takeover_fee; + + } + + } + + return (queued_funds, payee); + + } + + /// @notice Convenience function to assign bounties/bonds for multiple questions in one go, then withdraw all your funds. + /// Caller must provide the answer history for each question, in reverse order + /// @dev Can be called by anyone to assign bonds/bounties, but funds are only withdrawn for the user making the call. + /// @param question_ids The IDs of the questions you want to claim for + /// @param lengths The number of history entries you will supply for each question ID + /// @param hist_hashes In a single list for all supplied questions, the hash of each history entry. + /// @param addrs In a single list for all supplied questions, the address of each answerer or commitment sender + /// @param bonds In a single list for all supplied questions, the bond supplied with each answer or commitment + /// @param answers In a single list for all supplied questions, each answer supplied, or commitment ID + function claimMultipleAndWithdrawBalance( + bytes32[] memory question_ids, uint256[] memory lengths, + bytes32[] memory hist_hashes, address[] memory addrs, uint256[] memory bonds, bytes32[] memory answers + ) + stateAny() // The finalization checks are done in the claimWinnings function + public { + + uint256 qi; + uint256 i; + for (qi = 0; qi < question_ids.length; qi++) { + bytes32 qid = question_ids[qi]; + uint256 ln = lengths[qi]; + bytes32[] memory hh = new bytes32[](ln); + address[] memory ad = new address[](ln); + uint256[] memory bo = new uint256[](ln); + bytes32[] memory an = new bytes32[](ln); + uint256 j; + for (j = 0; j < ln; j++) { + hh[j] = hist_hashes[i]; + ad[j] = addrs[i]; + bo[j] = bonds[i]; + an[j] = answers[i]; + i++; + } + claimWinnings(qid, hh, ad, bo, an); + } + withdraw(); + } + + /// @notice Returns the questions's content hash, identifying the question content + /// @param question_id The ID of the question + function getContentHash(bytes32 question_id) + public view returns(bytes32) { + return questions[question_id].content_hash; + } + + /// @notice Returns the arbitrator address for the question + /// @param question_id The ID of the question + function getArbitrator(bytes32 question_id) + public view returns(address) { + return questions[question_id].arbitrator; + } + + /// @notice Returns the timestamp when the question can first be answered + /// @param question_id The ID of the question + function getOpeningTS(bytes32 question_id) + public view returns(uint32) { + return questions[question_id].opening_ts; + } + + /// @notice Returns the timeout in seconds used after each answer + /// @param question_id The ID of the question + function getTimeout(bytes32 question_id) + public view returns(uint32) { + return questions[question_id].timeout; + } + + /// @notice Returns the timestamp at which the question will be/was finalized + /// @param question_id The ID of the question + function getFinalizeTS(bytes32 question_id) + public view returns(uint32) { + return questions[question_id].finalize_ts; + } + + /// @notice Returns whether the question is pending arbitration + /// @param question_id The ID of the question + function isPendingArbitration(bytes32 question_id) + public view returns(bool) { + return questions[question_id].is_pending_arbitration; + } + + /// @notice Returns the current total unclaimed bounty + /// @dev Set back to zero once the bounty has been claimed + /// @param question_id The ID of the question + function getBounty(bytes32 question_id) + public view returns(uint256) { + return questions[question_id].bounty; + } + + /// @notice Returns the current best answer + /// @param question_id The ID of the question + function getBestAnswer(bytes32 question_id) + public view returns(bytes32) { + return questions[question_id].best_answer; + } + + /// @notice Returns the history hash of the question + /// @param question_id The ID of the question + /// @dev Updated on each answer, then rewound as each is claimed + function getHistoryHash(bytes32 question_id) + public view returns(bytes32) { + return questions[question_id].history_hash; + } + + /// @notice Returns the highest bond posted so far for a question + /// @param question_id The ID of the question + function getBond(bytes32 question_id) + public view returns(uint256) { + return questions[question_id].bond; + } + + /// @notice Returns the minimum bond that can answer the question + /// @param question_id The ID of the question + function getMinBond(bytes32 question_id) + public view returns(uint256) { + return questions[question_id].min_bond; + } + +} diff --git a/contracts/mixin/MoneyBox.sol b/contracts/mixin/MoneyBox.sol new file mode 100644 index 00000000..0048efd3 --- /dev/null +++ b/contracts/mixin/MoneyBox.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +/* + Contract to hold funds for the duration of an asset bridge -> send +*/ + +import {IERC20} from "./../interfaces/IERC20.sol"; + +contract MoneyBox { + + constructor(address _token) { + IERC20(_token).approve(msg.sender, type(uint256).max); + } + +} diff --git a/lib/zkevm-contracts b/lib/zkevm-contracts index 4c90c6a6..5d70866e 160000 --- a/lib/zkevm-contracts +++ b/lib/zkevm-contracts @@ -1 +1 @@ -Subproject commit 4c90c6a6ed347a049a2a50f0e94caba9471747e6 +Subproject commit 5d70866e1e4811df16d4c5fefb95100c962cc009 diff --git a/test/AdjudicationIntegration.t.sol b/test/AdjudicationIntegration.t.sol new file mode 100644 index 00000000..4401c652 --- /dev/null +++ b/test/AdjudicationIntegration.t.sol @@ -0,0 +1,517 @@ +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"; + +// TODO: Replace this with whatever zkEVM or whatever platform we're on uses +import {IAMB} from "../contracts/interfaces/IAMB.sol"; + +import {IRealityETH} from "../contracts/interfaces/IRealityETH.sol"; +import {IERC20} from "../contracts/interfaces/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 {L1GlobalChainInfoPublisher} from "../contracts/L1GlobalChainInfoPublisher.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; + + AdjudicationFramework internal adjudicationFramework1; + AdjudicationFramework 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 constant internal 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 AdjudicationFramework(address(l2RealityEth), 123, address(l2ForkArbitrator), initialArbitrators); + + 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 beginAddArbitratorToWhitelist + // For the setup we'll do this as an uncontested addition. + // Contested cases should also be tested. + + addArbitratorQID1 = adjudicationFramework1.beginAddArbitratorToAllowList(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.executeAddArbitratorToAllowList(addArbitratorQID1); + + skip(86401); + adjudicationFramework1.executeAddArbitratorToAllowList(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. + + } + + function _setupContestableQuestion() + internal returns (bytes32) { + + // ask a question + bytes32 qid = l2RealityEth.askQuestion(0, "Question 1", address(adjudicationFramework1), 123, uint32(block.timestamp), 0); + + // do some bond escalation + vm.prank(user1); + l2RealityEth.submitAnswer{value: 10}(qid, bytes32(uint256(1)), 0); + vm.prank(user2); + l2RealityEth.submitAnswer{value: 20}(qid, bytes32(0), 0); + + return qid; + + } + + function _setupArbitratedQuestion(bytes32 questionId) + internal { + + // request adjudication from the framework + vm.prank(user1); + assertTrue(adjudicationFramework1.requestArbitration{value: 500000}(questionId, 0)); + + vm.expectRevert("Arbitrator not allowlisted"); + l2Arbitrator2.requestArbitration{value: 500000}(questionId, 0); + + } + + function testL2RequestQuestionArbitration() public { + + bytes32 qid = _setupContestableQuestion(); + _setupArbitratedQuestion(qid); + + // permitted arbitrator grabs the question off the queue and locks it so nobody else can get it + assertTrue(l2Arbitrator1.requestArbitration{value: 500000}(qid, 0)); + l2Arbitrator1.submitAnswerByArbitrator(qid, bytes32(uint256(1)), user1); + + vm.expectRevert("Challenge deadline not passed"); + adjudicationFramework1.completeArbitration(qid, bytes32(uint256(1)), user1); + + skip(86401); + adjudicationFramework1.completeArbitration(qid, bytes32(uint256(1)), user1); + + assertEq(l2RealityEth.resultFor(qid), bytes32(uint256(1)), "reality.eth question should be settled"); + + } + + function _setupContestedArbitration() internal returns (bytes32 questionId, bytes32 removalQuestionId, bytes32 lastHistoryHash, bytes32 lastAnswer, address lastAnswerer) { + + bytes32 qid = _setupContestableQuestion(); + _setupArbitratedQuestion(qid); + + // TODO: Separate this part out to reuse in different tests for uncontested freeze and fork? + // permitted arbitrator grabs the question off the queue and locks it so nobody else can get it + assertTrue(l2Arbitrator1.requestArbitration{value: 500000}(qid, 0)); + l2Arbitrator1.submitAnswerByArbitrator(qid, bytes32(uint256(1)), user1); + + vm.expectRevert("Challenge deadline not passed"); + adjudicationFramework1.completeArbitration(qid, bytes32(uint256(1)), user1); + + // now before we can complete this somebody challenges it + removalQuestionId = adjudicationFramework1.beginRemoveArbitratorFromAllowList(address(l2Arbitrator1)); + l2RealityEth.submitAnswer{value: 10000}(removalQuestionId, bytes32(uint256(1)), 0); + + bytes32[] memory hashes; + address[] memory users; + uint256[] memory bonds; + bytes32[] memory answers; + + vm.expectRevert("Bond too low to freeze"); + adjudicationFramework1.freezeArbitrator(removalQuestionId, hashes, users, bonds, answers); + + lastHistoryHash = l2RealityEth.getHistoryHash(removalQuestionId); + vm.prank(user2); + l2RealityEth.submitAnswer{value: 20000}(removalQuestionId, bytes32(uint256(1)), 0); + adjudicationFramework1.freezeArbitrator(removalQuestionId, hashes, users, bonds, answers); + assertEq(adjudicationFramework1.countArbitratorFreezePropositions(address(l2Arbitrator1)), uint256(1)); + + //skip(86401); + //vm.expectRevert("Arbitrator must not be under dispute"); + //adjudicationFramework1.completeArbitration(qid, bytes32(uint256(1)), user1); + + /* Next step + a) Make a governance proposition on L1, escalating to a fork + b) Make a direct fork request on L1 via the bridge + c) Make a fork request on L2, get the response via the bridge + */ + + return (qid, removalQuestionId, lastHistoryHash, bytes32(uint256(1)), user2); + + } + + function testArbitrationContestPassedWithoutFork() public { + + // (bytes32 qid, bytes32 removalQuestionId, bytes32 lastHistoryHash, bytes32 lastAnswer, address lastAnswerer) = _setupContestedArbitration(); + (, bytes32 removalQuestionId, , , ) = _setupContestedArbitration(); + + // Currently in the "yes" state, so once it times out we can complete the removal + + // Now wait for the timeout and settle the proposition + vm.expectRevert("question must be finalized"); + l2RealityEth.resultFor(removalQuestionId); + + vm.expectRevert("question must be finalized"); + adjudicationFramework1.executeRemoveArbitratorFromAllowList(removalQuestionId); + + skip(86401); + + adjudicationFramework1.executeRemoveArbitratorFromAllowList(removalQuestionId); + + } + + function testArbitrationContestRejectedWithoutFork() public { + + //(bytes32 qid, bytes32 removalQuestionId, bytes32 lastHistoryHash, bytes32 lastAnswer, address lastAnswerer) = _setupContestedArbitration(); + (, bytes32 removalQuestionId, , , ) = _setupContestedArbitration(); + + // Put the proposition to remove the arbitrator into the "no" state + l2RealityEth.submitAnswer{value: 40000}(removalQuestionId, bytes32(uint256(0)), 0); + + // Now wait for the timeout and settle the proposition + + vm.expectRevert("question must be finalized"); + l2RealityEth.resultFor(removalQuestionId); + + vm.expectRevert("question must be finalized"); + adjudicationFramework1.executeRemoveArbitratorFromAllowList(removalQuestionId); + + skip(86401); + + vm.expectRevert("Result was not 1"); + adjudicationFramework1.executeRemoveArbitratorFromAllowList(removalQuestionId); + + assertEq(adjudicationFramework1.countArbitratorFreezePropositions(address(l2Arbitrator1)), uint256(1)); + adjudicationFramework1.clearFailedProposition(removalQuestionId); + assertEq(adjudicationFramework1.countArbitratorFreezePropositions(address(l2Arbitrator1)), uint256(0)); + + } + + function testArbitrationContestPassedWithFork() public { + + // (bytes32 qid, bytes32 removalQuestionId, bytes32 lastHistoryHash, bytes32 lastAnswer, address lastAnswerer) = _setupContestedArbitration(); + (, bytes32 removalQuestionId, bytes32 lastHistoryHash, bytes32 lastAnswer, address lastAnswerer) = _setupContestedArbitration(); + + // Currently in the "yes" state, so once it times out we can complete the removal + + // Now wait for the timeout and settle the proposition + vm.expectRevert("question must be finalized"); + l2RealityEth.resultFor(removalQuestionId); + + assertEq(address(l2ForkArbitrator.realitio()), address(l2RealityEth), "l2ForkArbitrator expects to arbitrate our l2RealityEth"); + assertEq(address(adjudicationFramework1.realityETH()), address(l2RealityEth), "adjudicationFramework1 expects to use our l2RealityEth"); + assertEq(address(l2ForkArbitrator), l2RealityEth.getArbitrator(removalQuestionId), "Arbitrator of the removalQuestionId is l2ForkArbitrator"); + + uint256 forkFee = l2ForkArbitrator.getDisputeFee(removalQuestionId); + l2ForkArbitrator.requestArbitration{value: forkFee}(removalQuestionId, 0); + + // IMAGINE THE FORK HAPPENED HERE + // There are now two L2s, each with a different chain ID + uint256 newChainId1 = 123; + vm.chainId(newChainId1); + + // TODO: Adjust the forkingFee as the total supply has changed a bit + bytes memory fakeMessageData = abi.encode(uint64(newChainId1), address(l1ForkingManagerF1), uint256(forkingFee), false, address(l2ForkArbitrator), removalQuestionId, bytes32(uint256(1))); + l2Bridge.fakeClaimMessage(address(l1GlobalChainInfoPublisher), uint32(0), address(l2ChainInfo), fakeMessageData, uint256(0)); + + assertTrue(l2RealityEth.isPendingArbitration(removalQuestionId)); + l2ForkArbitrator.handleCompletedFork(removalQuestionId, lastHistoryHash, lastAnswer, lastAnswerer); + + assertFalse(l2RealityEth.isPendingArbitration(removalQuestionId)); + + assertEq(adjudicationFramework1.countArbitratorFreezePropositions(address(l2Arbitrator1)), 1); + assertTrue(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); + adjudicationFramework1.executeRemoveArbitratorFromAllowList(removalQuestionId); + assertFalse(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); + assertEq(adjudicationFramework1.countArbitratorFreezePropositions(address(l2Arbitrator1)), 0); + + // TODO: Retry the arbitration with a new arbitrator + + } + + function testArbitrationContestRejectedWithFork() public { + + //(bytes32 qid, bytes32 removalQuestionId, bytes32 lastHistoryHash, bytes32 lastAnswer, address lastAnswerer) = _setupContestedArbitration(); + (, bytes32 removalQuestionId, bytes32 lastHistoryHash, bytes32 lastAnswer, address lastAnswerer) = _setupContestedArbitration(); + + // Currently in the "yes" state, so once it times out we can complete the removal + + // Now wait for the timeout and settle the proposition + vm.expectRevert("question must be finalized"); + bytes32 result = l2RealityEth.resultFor(removalQuestionId); + assertEq(result, bytes32(uint256(0))); + + assertEq(address(l2ForkArbitrator.realitio()), address(l2RealityEth), "l2ForkArbitrator expects to arbitrate our l2RealityEth"); + assertEq(address(adjudicationFramework1.realityETH()), address(l2RealityEth), "adjudicationFramework1 expects to use our l2RealityEth"); + assertEq(address(l2ForkArbitrator), l2RealityEth.getArbitrator(removalQuestionId), "Arbitrator of the removalQuestionId is l2ForkArbitrator"); + + uint256 forkFee = l2ForkArbitrator.getDisputeFee(removalQuestionId); + l2ForkArbitrator.requestArbitration{value: forkFee}(removalQuestionId, 0); + + // IMAGINE THE FORK HAPPENED HERE + // There are now two L2s, each with a different chain ID + uint256 newChainId1 = 124; + vm.chainId(newChainId1); + + // TODO: Adjust the forkingFee as the total supply has changed a bit + bytes memory fakeMessageData = abi.encode(uint64(newChainId1), address(l1ForkingManagerF1), uint256(forkingFee), false, address(l2ForkArbitrator), removalQuestionId, bytes32(uint256(0))); + l2Bridge.fakeClaimMessage(address(l1GlobalChainInfoPublisher), uint32(0), address(l2ChainInfo), fakeMessageData, uint256(0)); + + assertTrue(l2RealityEth.isPendingArbitration(removalQuestionId)); + l2ForkArbitrator.handleCompletedFork(removalQuestionId, lastHistoryHash, lastAnswer, lastAnswerer); + + assertFalse(l2RealityEth.isPendingArbitration(removalQuestionId)); + + assertEq(adjudicationFramework1.countArbitratorFreezePropositions(address(l2Arbitrator1)), 1); + assertTrue(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); + + vm.expectRevert("Result was not 1"); + adjudicationFramework1.executeRemoveArbitratorFromAllowList(removalQuestionId); + + adjudicationFramework1.clearFailedProposition(removalQuestionId); + + assertTrue(adjudicationFramework1.arbitrators(address(l2Arbitrator1))); + assertEq(adjudicationFramework1.countArbitratorFreezePropositions(address(l2Arbitrator1)), 0); + + } + + function testArbitrationContestForkFailed() public { + + (, bytes32 removalQuestionId, , , ) = _setupContestedArbitration(); + + // Currently in the "yes" state, so once it times out we can complete the removal + + // Now wait for the timeout and settle the proposition + vm.expectRevert("question must be finalized"); + bytes32 result = l2RealityEth.resultFor(removalQuestionId); + assertEq(result, bytes32(uint256(0))); + + assertEq(address(l2ForkArbitrator.realitio()), address(l2RealityEth), "l2ForkArbitrator expects to arbitrate our l2RealityEth"); + assertEq(address(adjudicationFramework1.realityETH()), address(l2RealityEth), "adjudicationFramework1 expects to use our l2RealityEth"); + assertEq(address(l2ForkArbitrator), l2RealityEth.getArbitrator(removalQuestionId), "Arbitrator of the removalQuestionId is l2ForkArbitrator"); + + uint256 forkFee = l2ForkArbitrator.getDisputeFee(removalQuestionId); + vm.prank(user2); + l2ForkArbitrator.requestArbitration{value: forkFee}(removalQuestionId, 0); + + assertTrue(l2ForkArbitrator.isForkInProgress(), "In forking state"); + + // L1 STUFF HAPPENS HERE + // Assume somebody else called fork or the fee changed or something. + // We should get a reply via the bridge. + + // NB Here we're sending the payment directly + // In fact it seems like it would have to be claimed separately + assertEq(address(l2ForkArbitrator).balance, 0); + payable(address(l2Bridge)).transfer(1000000); // Fund it so it can fund the L2ForkArbitrator + bytes memory fakeMessageData = abi.encode(removalQuestionId); + l2Bridge.fakeClaimMessage(address(l1GlobalForkRequester), uint32(0), address(l2ForkArbitrator), fakeMessageData, forkFee); + assertEq(address(l2ForkArbitrator).balance, forkFee); + + assertFalse(l2ForkArbitrator.isForkInProgress(), "Not in forking state"); + + l2ForkArbitrator.cancelArbitration(removalQuestionId); + assertEq(forkFee, l2ForkArbitrator.refundsDue(user2)); + + uint256 user2Bal = user2.balance; + vm.prank(user2); + l2ForkArbitrator.claimRefund(); + assertEq(address(l2ForkArbitrator).balance, 0); + 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 + govArb.setDisputeFee(50); + vm.mockCall( + address(l1RealityEth), + abi.encodeWithSelector(IRealityETH.isFinalized.selector), + abi.encode(true) + ); + assertTrue(govArb.requestArbitration{value: 50}(questionId, 0)); + assertEq(govArb.arbitration_bounties(questionId), 50); + } + */ + + + +} diff --git a/test/Arbitrator.t.sol b/test/Arbitrator.t.sol index 129c3ad4..8fed6656 100644 --- a/test/Arbitrator.t.sol +++ b/test/Arbitrator.t.sol @@ -1,6 +1,6 @@ pragma solidity ^0.8.20; import {Test} from "forge-std/Test.sol"; -import {Arbitrator} from "../contracts/Arbitrator.sol"; +import {Arbitrator} from "../contracts/lib/reality-eth/Arbitrator.sol"; import {IRealityETH} from "../contracts/interfaces/IRealityETH.sol"; import {IERC20} from "../contracts/interfaces/IERC20.sol"; diff --git a/test/ForkingManager.t.sol b/test/ForkingManager.t.sol index 57c1d524..6bc3f742 100644 --- a/test/ForkingManager.t.sol +++ b/test/ForkingManager.t.sol @@ -1,5 +1,7 @@ pragma solidity ^0.8.20; +/* solhint-disable not-rely-on-time */ + import {Test} from "forge-std/Test.sol"; import {ForkingManager} from "../contracts/ForkingManager.sol"; import {ForkableBridge} from "../contracts/ForkableBridge.sol"; @@ -73,12 +75,14 @@ contract ForkingManagerTest is Test { address(new ForkonomicToken()); address public disputeContract = address(0x1234567890123456789012345678901234567894); - bytes public disputeCall = "0x34567890129"; + bytes32 public disputeContent = "0x34567890129"; + bool public isL1 = true; ForkingManager.DisputeData public disputeData = IForkingManager.DisputeData({ disputeContract: disputeContract, - disputeCall: disputeCall + disputeContent: disputeContent, + isL1: isL1 }); event Transfer(address indexed from, address indexed to, uint256 tokenId); @@ -193,13 +197,62 @@ contract ForkingManagerTest is Test { ); } + function testForkingStatusFunctions() public { + + assertFalse(forkmanager.isForkingInitiated()); + assertFalse(forkmanager.isForkingExecuted()); + assertTrue(forkmanager.canFork()); + + // Mint and approve the arbitration fee for the test contract + forkonomicToken.approve(address(forkmanager), arbitrationFee); + vm.prank(address(this)); + forkonomicToken.mint(address(this), arbitrationFee); + + vm.expectEmit(true, true, true, true, address(forkonomicToken)); + emit Transfer( + address(this), + address(forkmanager), + uint256(arbitrationFee) + ); + + forkmanager.initiateFork( + IForkingManager.DisputeData({ + disputeContract: disputeContract, + disputeContent: disputeContent, + isL1: isL1 + }), + IForkingManager.NewImplementations({ + bridgeImplementation: newBridgeImplementation, + zkEVMImplementation: newZkevmImplementation, + forkonomicTokenImplementation: newForkonomicTokenImplementation, + forkingManagerImplementation: newForkmanagerImplementation, + globalExitRootImplementation: newGlobalExitRootImplementation, + verifier: newVerifierImplementation, + forkID: newForkID + }) + ); + + assertTrue(forkmanager.isForkingInitiated()); + assertFalse(forkmanager.isForkingExecuted()); + assertFalse(forkmanager.canFork()); + + vm.warp(block.timestamp + forkmanager.forkPreparationTime() + 1); + forkmanager.executeFork(); + + assertTrue(forkmanager.isForkingInitiated()); + assertTrue(forkmanager.isForkingExecuted()); + assertFalse(forkmanager.canFork()); + + } + function testInitiateForkChargesFees() public { // Call the initiateFork function to create a new fork vm.expectRevert(bytes("ERC20: insufficient allowance")); forkmanager.initiateFork( IForkingManager.DisputeData({ disputeContract: disputeContract, - disputeCall: disputeCall + disputeContent: disputeContent, + isL1: isL1 }), IForkingManager.NewImplementations({ bridgeImplementation: newBridgeImplementation, @@ -227,7 +280,8 @@ contract ForkingManagerTest is Test { forkmanager.initiateFork( IForkingManager.DisputeData({ disputeContract: disputeContract, - disputeCall: disputeCall + disputeContent: disputeContent, + isL1: isL1 }), IForkingManager.NewImplementations({ bridgeImplementation: newBridgeImplementation, @@ -398,6 +452,159 @@ contract ForkingManagerTest is Test { } } + function testInitiateForkAndExecuteWorksWithoutChangingImplementations() public { + // Mint and approve the arbitration fee for the test contract + forkonomicToken.approve(address(forkmanager), arbitrationFee); + vm.prank(address(this)); + forkonomicToken.mint(address(this), arbitrationFee); + + IForkingManager.NewImplementations memory noNewImplementations; + // Call the initiateFork function to create a new fork + forkmanager.initiateFork( + disputeData, + noNewImplementations + ); + vm.warp(block.timestamp + forkmanager.forkPreparationTime() + 1); + forkmanager.executeFork(); + + // Fetch the children from the ForkingManager + (address childForkmanager1, address childForkmanager2) = forkmanager + .getChildren(); + + // Assert that the fork managers implementation match the ones we provided + assertEq( + bytesToAddress( + vm.load(address(childForkmanager1), _IMPLEMENTATION_SLOT) + ), + forkmanagerImplementation + ); + assertEq( + bytesToAddress( + vm.load(address(childForkmanager2), _IMPLEMENTATION_SLOT) + ), + forkmanagerImplementation + ); + + { + // Fetch the children from the ForkableBridge contract + (address childBridge1, address childBridge2) = bridge.getChildren(); + + // Assert that the bridges match the ones we provided + assertEq( + bytesToAddress( + vm.load(address(childBridge1), _IMPLEMENTATION_SLOT) + ), + bridgeImplementation + ); + assertEq( + bytesToAddress( + vm.load(address(childBridge2), _IMPLEMENTATION_SLOT) + ), + bridgeImplementation + ); + } + { + // Fetch the children from the ForkableZkEVM contract + (address childZkevm1, address childZkevm2) = zkevm.getChildren(); + + // Assert that the ZkEVM contracts match the ones we provided + assertEq( + bytesToAddress( + vm.load(address(childZkevm1), _IMPLEMENTATION_SLOT) + ), + zkevmImplementation + ); + assertEq( + bytesToAddress( + vm.load(address(childZkevm2), _IMPLEMENTATION_SLOT) + ), + zkevmImplementation + ); + (address childBridge1, address childBridge2) = bridge.getChildren(); + assertEq( + ForkableBridge(childBridge1).polygonZkEVMaddress(), + childZkevm1 + ); + assertEq( + ForkableBridge(childBridge2).polygonZkEVMaddress(), + childZkevm2 + ); + assertEq(ForkableZkEVM(childZkevm1).chainID(), firstChainId); + assertEq(ForkableZkEVM(childZkevm2).chainID(), secondChainId); + assertEq( + ForkableZkEVM(childZkevm1).forkID(), + ForkableZkEVM(zkevm).forkID() + ); + assertEq( + ForkableZkEVM(childZkevm2).forkID(), + ForkableZkEVM(zkevm).forkID() + ); + } + { + // Fetch the children from the ForkonomicToken contract + ( + address childForkonomicToken1, + address childForkonomicToken2 + ) = forkonomicToken.getChildren(); + + // Assert that the forkonomic tokens match the ones we provided + assertEq( + bytesToAddress( + vm.load( + address(childForkonomicToken1), + _IMPLEMENTATION_SLOT + ) + ), + forkonomicTokenImplementation + ); + assertEq( + bytesToAddress( + vm.load( + address(childForkonomicToken2), + _IMPLEMENTATION_SLOT + ) + ), + forkonomicTokenImplementation + ); + } + { + // Fetch the children from the ForkonomicToken contract + ( + address childGlobalExitRoot1, + address childGlobalExitRoot2 + ) = globalExitRoot.getChildren(); + + // Assert that the forkonomic tokens match the ones we provided + assertEq( + bytesToAddress( + vm.load(address(childGlobalExitRoot1), _IMPLEMENTATION_SLOT) + ), + globalExitRootImplementation + ); + assertEq( + bytesToAddress( + vm.load(address(childGlobalExitRoot2), _IMPLEMENTATION_SLOT) + ), + globalExitRootImplementation + ); + + assertEq( + ForkableGlobalExitRoot(childGlobalExitRoot1).forkmanager(), + childForkmanager1 + ); + } + { + assertEq( + chainIdManagerAddress, + ForkingManager(childForkmanager1).chainIdManager() + ); + assertEq( + chainIdManagerAddress, + ForkingManager(childForkmanager2).chainIdManager() + ); + } + } + function testInitiateForkSetsDispuateDataAndExecutionTime() public { // Mint and approve the arbitration fee for the test contract forkonomicToken.approve(address(forkmanager), arbitrationFee); @@ -421,17 +628,18 @@ contract ForkingManagerTest is Test { ); skip(forkmanager.forkPreparationTime() + 1); forkmanager.executeFork(); - ( + bool receivedIsL1, address receivedDisputeContract, - bytes memory receivedDisputeCall + bytes32 receivedDisputeContent ) = ForkingManager(forkmanager).disputeData(); uint256 receivedExecutionTime = ForkingManager(forkmanager) .executionTimeForProposal(); // Assert the dispute contract and call stored in the ForkingManager match the ones we provided assertEq(receivedDisputeContract, disputeContract); - assertEq(receivedDisputeCall, disputeCall); + assertEq(receivedDisputeContent, disputeContent); + assertEq(receivedIsL1, isL1); assertEq( receivedExecutionTime, testTimestamp + forkmanager.forkPreparationTime() @@ -504,7 +712,7 @@ contract ForkingManagerTest is Test { forkonomicToken.mint(address(this), 3 * arbitrationFee); // Call the initiateFork function to create a new fork - disputeData.disputeCall = "0x1"; + disputeData.disputeContent = "0x1"; forkmanager.initiateFork( disputeData, IForkingManager.NewImplementations({ @@ -517,7 +725,7 @@ contract ForkingManagerTest is Test { forkID: newForkID }) ); - disputeData.disputeCall = "0x2"; + disputeData.disputeContent = "0x2"; vm.expectRevert(bytes("ForkingManager: fork pending")); forkmanager.initiateFork( disputeData, diff --git a/test/L1GlobalChainInfoPublisher.t.sol b/test/L1GlobalChainInfoPublisher.t.sol new file mode 100644 index 00000000..6d7496b6 --- /dev/null +++ b/test/L1GlobalChainInfoPublisher.t.sol @@ -0,0 +1,339 @@ +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"; + +// TODO: Replace this with whatever zkEVM or whatever platform we're on uses +import {IAMB} from "../contracts/interfaces/IAMB.sol"; + +import {IRealityETH} from "../contracts/interfaces/IRealityETH.sol"; +import {IERC20} from "../contracts/interfaces/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 {L1GlobalChainInfoPublisher} from "../contracts/L1GlobalChainInfoPublisher.sol"; +import {L1GlobalForkRequester} from "../contracts/L1GlobalForkRequester.sol"; +import {L2ChainInfo} from "../contracts/L2ChainInfo.sol"; + +import {MockPolygonZkEVMBridge} from "./testcontract/MockPolygonZkEVMBridge.sol"; + +pragma solidity ^0.8.20; + +/* solhint-disable not-rely-on-time */ + +import {Test} from "forge-std/Test.sol"; +import {ForkingManager} from "../contracts/ForkingManager.sol"; +import {ForkableBridge} from "../contracts/ForkableBridge.sol"; +import {ForkableZkEVM} from "../contracts/ForkableZkEVM.sol"; +import {ForkonomicToken} from "../contracts/ForkonomicToken.sol"; +import {ForkableGlobalExitRoot} from "../contracts/ForkableGlobalExitRoot.sol"; +import {IBasePolygonZkEVMGlobalExitRoot} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMGlobalExitRoot.sol"; +import {IForkingManager} from "../contracts/interfaces/IForkingManager.sol"; +import {IVerifierRollup} from "@RealityETH/zkevm-contracts/contracts/interfaces/IVerifierRollup.sol"; +import {IPolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMBridge.sol"; +import {PolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/inheritedMainContracts/PolygonZkEVMBridge.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC20Upgradeable.sol"; +import {IPolygonZkEVM} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVM.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ChainIdManager} from "../contracts/ChainIdManager.sol"; +import {ForkableZkEVM} from "../contracts/ForkableZkEVM.sol"; + +contract L1GlobalChainInfoPublisherTest is Test { + + ForkableBridge public bridge; + ForkonomicToken public forkonomicToken; + ForkingManager public forkmanager; + ForkableZkEVM public zkevm; + ForkableGlobalExitRoot public globalExitRoot; + + ForkableBridge public l2Bridge; + + address public bridgeImplementation; + address public forkmanagerImplementation; + address public zkevmImplementation; + address public forkonomicTokenImplementation; + address public globalExitRootImplementation; + address public chainIdManagerAddress; + bytes32 internal constant _IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + bytes32 public genesisRoot = + bytes32( + 0x827a9240c96ccb855e4943cc9bc49a50b1e91ba087007441a1ae5f9df8d1c57c + ); + uint64 public forkID = 3; + uint64 public newForkID = 4; + uint32 public networkID = 10; + uint64 public pendingStateTimeout = 123; + uint64 public trustedAggregatorTimeout = 124235; + address public hardAssetManger = + address(0x1234567890123456789012345678901234567891); + address public trustedSequencer = + address(0x1234567890123456789012345678901234567899); + address public trustedAggregator = + address(0x1234567890123456789012345678901234567898); + IVerifierRollup public rollupVerifierMock = + IVerifierRollup(0x1234567890123456789012345678901234567893); + uint256 public arbitrationFee = 1020; + bytes32[32] public depositTree; + address public admin = address(0xad); + uint64 public initialChainId = 1; + uint64 public firstChainId = initialChainId+1; + uint64 public secondChainId = initialChainId+2; + + // Setup new implementations for the fork + address public newBridgeImplementation = address(new ForkableBridge()); + address public newForkmanagerImplementation = address(new ForkingManager()); + address public newZkevmImplementation = address(new ForkableZkEVM()); + address public newVerifierImplementation = + address(0x1234567890123456789012345678901234567894); + address public newGlobalExitRootImplementation = + address(new ForkableGlobalExitRoot()); + address public newForkonomicTokenImplementation = + address(new ForkonomicToken()); + address public disputeContract = + address(0x1234567890123456789012345678901234567894); + bytes32 public disputeContent = "0x34567890129"; + bool public isL1 = true; + + L1GlobalChainInfoPublisher public l1GlobalChainInfoPublisher = new L1GlobalChainInfoPublisher(); + L2ChainInfo public l2ChainInfo = new L2ChainInfo(address(l2Bridge), address(l1GlobalChainInfoPublisher)); + + ForkingManager.DisputeData public disputeData = + IForkingManager.DisputeData({ + disputeContract: disputeContract, + disputeContent: disputeContent, + isL1: isL1 + }); + + event Transfer(address indexed from, address indexed to, uint256 tokenId); + + function bytesToAddress(bytes32 b) public pure returns (address) { + return address(uint160(uint256(b))); + } + + function setUp() public { + + bridgeImplementation = address(new ForkableBridge()); + bridge = ForkableBridge( + address( + new TransparentUpgradeableProxy(bridgeImplementation, admin, "") + ) + ); + + // Bridge on l2, should have different chain ID etc + l2Bridge = ForkableBridge( + address( + new TransparentUpgradeableProxy(bridgeImplementation, admin, "") + ) + ); + + forkmanagerImplementation = address(new ForkingManager()); + forkmanager = ForkingManager( + address( + new TransparentUpgradeableProxy( + forkmanagerImplementation, + admin, + "" + ) + ) + ); + zkevmImplementation = address(new ForkableZkEVM()); + zkevm = ForkableZkEVM( + address( + new TransparentUpgradeableProxy(zkevmImplementation, admin, "") + ) + ); + forkonomicTokenImplementation = address(new ForkonomicToken()); + forkonomicToken = ForkonomicToken( + address( + new TransparentUpgradeableProxy( + forkonomicTokenImplementation, + admin, + "" + ) + ) + ); + globalExitRootImplementation = address(new ForkableGlobalExitRoot()); + globalExitRoot = ForkableGlobalExitRoot( + address( + new TransparentUpgradeableProxy( + globalExitRootImplementation, + admin, + "" + ) + ) + ); + ChainIdManager chainIdManager= new ChainIdManager(initialChainId); + chainIdManagerAddress = address(chainIdManager); + globalExitRoot.initialize( + address(forkmanager), + address(0x0), + address(zkevm), + address(bridge) + ); + bridge.initialize( + address(forkmanager), + address(0x0), + networkID, + globalExitRoot, + address(zkevm), + address(forkonomicToken), + false, + hardAssetManger, + 0, + depositTree + ); + + IPolygonZkEVM.InitializePackedParameters + memory initializePackedParameters = IPolygonZkEVM + .InitializePackedParameters({ + admin: admin, + trustedSequencer: trustedSequencer, + pendingStateTimeout: pendingStateTimeout, + trustedAggregator: trustedAggregator, + trustedAggregatorTimeout: trustedAggregatorTimeout, + chainID: chainIdManager.getNextUsableChainId(), + forkID: forkID + }); + zkevm.initialize( + address(forkmanager), + address(0x0), + initializePackedParameters, + genesisRoot, + "trustedSequencerURL", + "test network", + "0.0.1", + globalExitRoot, + IERC20Upgradeable(address(forkonomicToken)), + rollupVerifierMock, + IPolygonZkEVMBridge(address(bridge)) + ); + forkmanager.initialize( + address(zkevm), + address(bridge), + address(forkonomicToken), + address(0x0), + address(globalExitRoot), + arbitrationFee, + chainIdManagerAddress + ); + forkonomicToken.initialize( + address(forkmanager), + address(0x0), + address(this), + "Fork", + "FORK" + ); + } + + function testChainInfoPublishedBeforeFork() public { + + // vm.recordLogs(); + l1GlobalChainInfoPublisher.updateL2ChainInfo(address(bridge), address(l2ChainInfo), address(0), 10); + // Vm.Log[] memory entries = vm.getRecordedLogs(); + // TODO: Check the logs + + } + + function testChainInfoPublishedBeforeForkBreaksWithBrokenBridge() public { + address garbageAddress = address(0xabcd01); + vm.expectRevert(); + l1GlobalChainInfoPublisher.updateL2ChainInfo(garbageAddress, address(l2ChainInfo), address(0), 10); + } + + function testChainInfoPublishedBeforeForkRevertsWithBrokenAncestor() public { + address garbageAddress = address(0xabcd01); + vm.expectRevert("Ancestor not found"); + l1GlobalChainInfoPublisher.updateL2ChainInfo(address(bridge), address(l2ChainInfo), garbageAddress, 10); + } + + function testChainInfoPublishedAfterForks() public { + // Mint and approve the arbitration fee for the test contract + // We'll do several for repeated forks + forkonomicToken.approve(address(forkmanager), arbitrationFee * 3); + vm.prank(address(this)); + forkonomicToken.mint(address(this), arbitrationFee * 3); + + // Call the initiateFork function to create a new fork + forkmanager.initiateFork( + disputeData, + IForkingManager.NewImplementations({ + bridgeImplementation: newBridgeImplementation, + zkEVMImplementation: newZkevmImplementation, + forkonomicTokenImplementation: newForkonomicTokenImplementation, + forkingManagerImplementation: newForkmanagerImplementation, + globalExitRootImplementation: newGlobalExitRootImplementation, + verifier: newVerifierImplementation, + forkID: newForkID + }) + ); + skip(forkmanager.forkPreparationTime() + 1); + forkmanager.executeFork(); + + // The current bridge should no longer work + vm.expectRevert("No changes after forking"); + l1GlobalChainInfoPublisher.updateL2ChainInfo(address(bridge), address(l2ChainInfo), address(0), uint256(10)); + + (address forkmanager1Addrg, address forkmanager2Addr) = forkmanager.getChildren(); + address bridge1 = IForkingManager(forkmanager1Addrg).bridge(); + address bridge2 = IForkingManager(forkmanager2Addr).bridge(); + + // The new bridges should work though + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge1, address(l2ChainInfo), address(0), uint256(10)); + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge2, address(l2ChainInfo), address(0), uint256(10)); + + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge1, address(l2ChainInfo), address(forkmanager), uint256(10)); + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge2, address(l2ChainInfo), address(forkmanager), uint256(10)); + + ForkingManager forkmanager2 = ForkingManager(forkmanager2Addr); + ForkonomicToken forkonomicToken2 = ForkonomicToken(forkmanager2.forkonomicToken()); + + // Next we'll fork with a dispute + ForkingManager.DisputeData memory disputeData2 = IForkingManager.DisputeData({ + disputeContract: address(0xabab), + disputeContent: bytes32("0xbaba"), + isL1: true + }); + + IForkingManager.NewImplementations memory newImplementations2; // Empty one to simulate a question + + forkonomicToken.splitTokensIntoChildTokens(arbitrationFee); + forkonomicToken2.approve(address(forkmanager2), arbitrationFee); + vm.prank(address(this)); + + // Call the initiateFork function to create a new fork + forkmanager2.initiateFork( + disputeData2, + newImplementations2 + ); + skip(forkmanager.forkPreparationTime() + 1); + forkmanager2.executeFork(); + + vm.expectRevert("No changes after forking"); + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge2, address(l2ChainInfo), address(forkmanager), uint256(10)); + + (, address forkmanager22Addr) = forkmanager2.getChildren(); + // address bridge21 = IForkingManager(forkmanager21Addrg).bridge(); + address bridge22 = IForkingManager(forkmanager22Addr).bridge(); + + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge22, address(l2ChainInfo), address(forkmanager), uint256(10)); + + vm.expectRevert("Ancestor not found"); + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge22, address(l2ChainInfo), address(forkmanager), uint256(0)); + vm.expectRevert("Ancestor not found"); + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge22, address(l2ChainInfo), address(forkmanager), uint256(1)); + + l1GlobalChainInfoPublisher.updateL2ChainInfo(bridge22, address(l2ChainInfo), address(forkmanager), uint256(2)); + + } + +} diff --git a/test/L1GlobalForkRequester.t.sol b/test/L1GlobalForkRequester.t.sol new file mode 100644 index 00000000..ddee52c5 --- /dev/null +++ b/test/L1GlobalForkRequester.t.sol @@ -0,0 +1,347 @@ +pragma solidity ^0.8.20; + +/* solhint-disable not-rely-on-time */ + +import {Test} from "forge-std/Test.sol"; +import {ForkingManager} from "../contracts/ForkingManager.sol"; +import {ForkableBridge} from "../contracts/ForkableBridge.sol"; +import {ForkableZkEVM} from "../contracts/ForkableZkEVM.sol"; +import {ForkonomicToken} from "../contracts/ForkonomicToken.sol"; +import {ForkableGlobalExitRoot} from "../contracts/ForkableGlobalExitRoot.sol"; +import {IBasePolygonZkEVMGlobalExitRoot} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMGlobalExitRoot.sol"; +import {IForkingManager} from "../contracts/interfaces/IForkingManager.sol"; +import {IVerifierRollup} from "@RealityETH/zkevm-contracts/contracts/interfaces/IVerifierRollup.sol"; +import {IPolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMBridge.sol"; +import {PolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/inheritedMainContracts/PolygonZkEVMBridge.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/interfaces/IERC20Upgradeable.sol"; +import {IPolygonZkEVM} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVM.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; +import {ChainIdManager} from "../contracts/ChainIdManager.sol"; +import {ForkableZkEVM} from "../contracts/ForkableZkEVM.sol"; + +import {L1GlobalForkRequester} from "../contracts/L1GlobalForkRequester.sol"; +import {ExampleMoneyBoxUser} from "./testcontract/ExampleMoneyBoxUser.sol"; + +contract L1GlobalForkRequesterTest is Test { + + L1GlobalForkRequester public l1GlobalForkRequester = new L1GlobalForkRequester(); + + ForkableBridge public bridge; + ForkonomicToken public forkonomicToken; + ForkingManager public forkmanager; + ForkableZkEVM public zkevm; + ForkableGlobalExitRoot public globalExitRoot; + + + address public bridgeImplementation; + address public forkmanagerImplementation; + address public zkevmImplementation; + address public forkonomicTokenImplementation; + address public globalExitRootImplementation; + address public chainIdManager; + bytes32 internal constant _IMPLEMENTATION_SLOT = + 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; + + IBasePolygonZkEVMGlobalExitRoot public globalExitMock = + IBasePolygonZkEVMGlobalExitRoot( + 0x1234567890123456789012345678901234567892 + ); + bytes32 public genesisRoot = + bytes32( + 0x827a9240c96ccb855e4943cc9bc49a50b1e91ba087007441a1ae5f9df8d1c57c + ); + uint64 public forkID = 3; + uint64 public newForkID = 4; + uint64 public chainID = 4; + uint32 public networkID = 10; + uint64 public pendingStateTimeout = 123; + uint64 public trustedAggregatorTimeout = 124235; + address public hardAssetManger = + address(0x1234567890123456789012345678901234567891); + address public trustedSequencer = + address(0x1234567890123456789012345678901234567899); + address public trustedAggregator = + address(0x1234567890123456789012345678901234567898); + IVerifierRollup public rollupVerifierMock = + IVerifierRollup(0x1234567890123456789012345678901234567893); + uint256 public arbitrationFee = 1020; + bytes32[32] public depositTree; + address public admin = address(0xad); + uint64 public initialChainId = 1; + uint64 public firstChainId = 1; + uint64 public secondChainId = 2; + + // Setup new implementations for the fork + address public newBridgeImplementation = address(new ForkableBridge()); + address public newForkmanagerImplementation = address(new ForkingManager()); + address public newZkevmImplementation = address(new ForkableZkEVM()); + address public newVerifierImplementation = + address(0x1234567890123456789012345678901234567894); + address public newGlobalExitRootImplementation = + address(new ForkableGlobalExitRoot()); + address public newForkonomicTokenImplementation = + address(new ForkonomicToken()); + address public disputeContract = + address(0x1234567890123456789012345678901234567894); + bytes32 public disputeContent = "0x34567890129"; + bool public isL1 = true; + + event Transfer(address indexed from, address indexed to, uint256 tokenId); + + function bytesToAddress(bytes32 b) public pure returns (address) { + return address(uint160(uint256(b))); + } + + // TODO: This setup is duplicated with the ForkingManager tests + // It might be good to pull it out somewhere. + function setUp() public { + bridgeImplementation = address(new ForkableBridge()); + bridge = ForkableBridge( + address( + new TransparentUpgradeableProxy(bridgeImplementation, admin, "") + ) + ); + forkmanagerImplementation = address(new ForkingManager()); + forkmanager = ForkingManager( + address( + new TransparentUpgradeableProxy( + forkmanagerImplementation, + admin, + "" + ) + ) + ); + zkevmImplementation = address(new ForkableZkEVM()); + zkevm = ForkableZkEVM( + address( + new TransparentUpgradeableProxy(zkevmImplementation, admin, "") + ) + ); + forkonomicTokenImplementation = address(new ForkonomicToken()); + forkonomicToken = ForkonomicToken( + address( + new TransparentUpgradeableProxy( + forkonomicTokenImplementation, + admin, + "" + ) + ) + ); + globalExitRootImplementation = address(new ForkableGlobalExitRoot()); + globalExitRoot = ForkableGlobalExitRoot( + address( + new TransparentUpgradeableProxy( + globalExitRootImplementation, + admin, + "" + ) + ) + ); + + chainIdManager = address(new ChainIdManager(initialChainId)); + globalExitRoot.initialize( + address(forkmanager), + address(0x0), + address(zkevm), + address(bridge) + ); + bridge.initialize( + address(forkmanager), + address(0x0), + networkID, + globalExitMock, + address(zkevm), + address(forkonomicToken), + false, + hardAssetManger, + 0, + depositTree + ); + + IPolygonZkEVM.InitializePackedParameters + memory initializePackedParameters = IPolygonZkEVM + .InitializePackedParameters({ + admin: admin, + trustedSequencer: trustedSequencer, + pendingStateTimeout: pendingStateTimeout, + trustedAggregator: trustedAggregator, + trustedAggregatorTimeout: trustedAggregatorTimeout, + chainID: chainID, + forkID: forkID + }); + zkevm.initialize( + address(forkmanager), + address(0x0), + initializePackedParameters, + genesisRoot, + "trustedSequencerURL", + "test network", + "0.0.1", + globalExitRoot, + IERC20Upgradeable(address(forkonomicToken)), + rollupVerifierMock, + IPolygonZkEVMBridge(address(bridge)) + ); + forkmanager.initialize( + address(zkevm), + address(bridge), + address(forkonomicToken), + address(0x0), + address(globalExitRoot), + arbitrationFee, + chainIdManager + ); + forkonomicToken.initialize( + address(forkmanager), + address(0x0), + address(this), + "Fork", + "FORK" + ); + } + + function testReceivePayment() public { + + uint256 fee = forkmanager.arbitrationFee(); + + ExampleMoneyBoxUser exampleMoneyBoxUser = new ExampleMoneyBoxUser(); + // Receive a payment from a MoneyBox + + address l2Requester = address(0xbabe01); + bytes32 requestId = bytes32("0xc0ffee01"); + bytes32 salt = keccak256(abi.encodePacked(l2Requester, requestId)); + address moneyBoxAddress = exampleMoneyBoxUser.calculateMoneyBoxAddress(address(l1GlobalForkRequester), salt, address(forkonomicToken)); + + vm.prank(address(this)); + forkonomicToken.mint(address(this), fee); + + vm.prank(address(this)); + forkonomicToken.transfer(moneyBoxAddress, fee); + + assertEq(address(forkmanager.forkonomicToken()), address(forkonomicToken)); + assertTrue(forkmanager.canFork()); + assertFalse(forkmanager.isForkingInitiated()); + assertFalse(forkmanager.isForkingExecuted()); + + l1GlobalForkRequester.handlePayment(address(forkonomicToken), l2Requester, requestId); + + assertTrue(forkmanager.isForkingInitiated()); + assertFalse(forkmanager.isForkingExecuted()); + + } + + function testReceiveInsufficientPayment() public { + + uint256 fee = forkmanager.arbitrationFee() - 1; + + ExampleMoneyBoxUser exampleMoneyBoxUser = new ExampleMoneyBoxUser(); + // Receive a payment from a MoneyBox + + address l2Requester = address(0xbabe01); + bytes32 requestId = bytes32("0xc0ffee01"); + bytes32 salt = keccak256(abi.encodePacked(l2Requester, requestId)); + address moneyBoxAddress = exampleMoneyBoxUser.calculateMoneyBoxAddress(address(l1GlobalForkRequester), salt, address(forkonomicToken)); + + vm.prank(address(this)); + forkonomicToken.mint(address(this), fee); + + vm.prank(address(this)); + forkonomicToken.transfer(moneyBoxAddress, fee); + + assertEq(address(forkmanager.forkonomicToken()), address(forkonomicToken)); + assertTrue(forkmanager.canFork()); + + l1GlobalForkRequester.handlePayment(address(forkonomicToken), l2Requester, requestId); + assertFalse(forkmanager.isForkingInitiated()); + + (uint256 amount, uint256 amountRemainingY, uint256 amountRemainingN) = l1GlobalForkRequester.failedRequests(address(forkonomicToken), l2Requester, requestId); + assertEq(amount, fee); + assertEq(amountRemainingY, 0); + assertEq(amountRemainingN, 0); + + } + + function testHandleOtherRequestForksFirst() public { + + uint256 fee = forkmanager.arbitrationFee(); + + ExampleMoneyBoxUser exampleMoneyBoxUser = new ExampleMoneyBoxUser(); + // Receive a payment from a MoneyBox + + address l2Requester = address(0xbabe01); + bytes32 requestId = bytes32("0xc0ffee01"); + bytes32 salt = keccak256(abi.encodePacked(l2Requester, requestId)); + address moneyBoxAddress = exampleMoneyBoxUser.calculateMoneyBoxAddress(address(l1GlobalForkRequester), salt, address(forkonomicToken)); + + vm.prank(address(this)); + forkonomicToken.mint(address(this), fee); + + vm.prank(address(this)); + forkonomicToken.transfer(moneyBoxAddress, fee); + + assertEq(address(forkmanager.forkonomicToken()), address(forkonomicToken)); + assertTrue(forkmanager.canFork()); + + { + // Someone else starts and executes a fork before we can handle our payment + vm.prank(address(this)); + forkonomicToken.mint(address(this), fee); + vm.prank(address(this)); + forkonomicToken.approve(address(forkmanager), fee); + // Assume the data contains the questionId and pass it directly to the forkmanager in the fork request + IForkingManager.NewImplementations memory newImplementations; + IForkingManager.DisputeData memory disputeData = IForkingManager.DisputeData(false, address(this), requestId); + forkmanager.initiateFork(disputeData, newImplementations); + } + + // Our handlePayment will fail and leave our money sitting in failedRequests + uint256 balBeforeHandle = forkonomicToken.balanceOf(address(l1GlobalForkRequester)); + + l1GlobalForkRequester.handlePayment(address(forkonomicToken), l2Requester, requestId); + (uint256 amount, uint256 amountRemainingY, uint256 amountRemainingN) = l1GlobalForkRequester.failedRequests(address(forkonomicToken), l2Requester, requestId); + assertEq(amount, fee); + assertEq(amountRemainingY, 0); + assertEq(amountRemainingN, 0); + + uint256 balAfterHandle = forkonomicToken.balanceOf(address(l1GlobalForkRequester)); + assertEq(balBeforeHandle + amount, balAfterHandle); + + vm.expectRevert("Token not forked"); + l1GlobalForkRequester.splitTokensIntoChildTokens(address(forkonomicToken), l2Requester, requestId); + + // Execute the other guy's fork + skip(forkmanager.forkPreparationTime() + 1); + forkmanager.executeFork(); + + { + uint256 balBeforeSplit = forkonomicToken.balanceOf(address(l1GlobalForkRequester)); + l1GlobalForkRequester.splitTokensIntoChildTokens(address(forkonomicToken), l2Requester, requestId); + uint256 balAfterSplit = forkonomicToken.balanceOf(address(l1GlobalForkRequester)); + assertEq(balAfterSplit + amount, balBeforeSplit); + } + + // The children should now both have the funds we split + (address childToken1, address childToken2) = forkonomicToken.getChildren(); + assertEq(ForkonomicToken(childToken1).balanceOf(address(l1GlobalForkRequester)), amount); + assertEq(ForkonomicToken(childToken2).balanceOf(address(l1GlobalForkRequester)), amount); + + // Now we should be able to return the tokens on the child chain + l1GlobalForkRequester.returnTokens(address(childToken1), l2Requester, requestId); + (uint256 amountChild1, , ) = l1GlobalForkRequester.failedRequests(childToken1, l2Requester, requestId); + (uint256 amountChild2, , ) = l1GlobalForkRequester.failedRequests(childToken2, l2Requester, requestId); + + assertEq(ForkonomicToken(childToken2).balanceOf(address(l1GlobalForkRequester)), amount); + + assertEq(amountChild1, 0); + assertEq(amountChild2, amount); + + // TODO: This breaks due to _CURRENT_SUPPORTED_NETWORKS which is capped at 2 + // Raise this if we need it, alternatively maybe it's unrelated to Chain ID and it doesn't need to change when the fork does. + + // l1GlobalForkRequester.returnTokens(address(childToken2), l2Requester, requestId); + // (amountChild2, , ) = l1GlobalForkRequester.failedRequests(childToken2, l2Requester, requestId); + // assertEq(amountChild2, 0); + + } + +} diff --git a/test/MoneyBox.t.sol b/test/MoneyBox.t.sol new file mode 100644 index 00000000..6e2f7434 --- /dev/null +++ b/test/MoneyBox.t.sol @@ -0,0 +1,75 @@ +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import {MoneyBox} from "../contracts/mixin/MoneyBox.sol"; +import {CalculateMoneyBoxAddress} from "../contracts/lib/CalculateMoneyBoxAddress.sol"; + +import {ExampleToken} from "./testcontract/ExampleToken.sol"; +import {ExampleMoneyBoxUser} from "./testcontract/ExampleMoneyBoxUser.sol"; + +contract MoneyBoxTest is Test { + + ExampleToken internal token; + bytes32 internal salt = bytes32("0xbabebabe"); + + address internal user1 = address(0xc0ffee01); + address internal user2 = address(0xc0ffee02); + + function setUp() public { + vm.prank(user1); + token = new ExampleToken(); + } + + function testCreatorApproved() public { + vm.recordLogs(); + vm.prank(user2); + new MoneyBox{salt: salt}(address(token)); + Vm.Log[] memory entries = vm.getRecordedLogs(); + address approvedUser = address(uint160(uint256(entries[0].topics[2]))); + assertEq(approvedUser, user2); + uint256 approveAmount = abi.decode(entries[0].data, (uint256)); + assertEq(approveAmount, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + } + + function testCreatorCanTakeTokens() public { + + // We'll credit the tokens before deploying the contract as this is the order it will be done in in real life + ExampleMoneyBoxUser exampleMoneyBoxUser = new ExampleMoneyBoxUser(); + address calculatedAddress = exampleMoneyBoxUser.calculateMoneyBoxAddress(user1, salt, address(token)); + token.fakeMint(calculatedAddress, 10000123); + + vm.prank(user1); + MoneyBox moneyBox = new MoneyBox{salt: salt}(address(token)); + vm.prank(user2); + vm.expectRevert(bytes("ERC20: insufficient allowance")); + token.transferFrom(address(moneyBox), user2, 123); + vm.prank(user1); + token.transferFrom(address(moneyBox), user2, 123); + + assertEq(token.balanceOf(address(moneyBox)), 10000123 - 123); + assertEq(token.balanceOf(user2), 123); + + } + + function testAddressCalculation() public { + + vm.prank(user2); + MoneyBox moneyBox = new MoneyBox{salt: salt}(address(token)); + ExampleMoneyBoxUser exampleMoneyBoxUser = new ExampleMoneyBoxUser(); + address calculatedAddress = exampleMoneyBoxUser.calculateMoneyBoxAddress(user2, salt, address(token)); + assertEq(calculatedAddress, address(moneyBox)); + + address calculatedAddress2 = exampleMoneyBoxUser.calculateMoneyBoxAddress(user1, salt, address(token)); + assertNotEq(calculatedAddress2, address(moneyBox)); + + address calculatedAddress3 = exampleMoneyBoxUser.calculateMoneyBoxAddress(user2, bytes32("0xee00bb"), address(token)); + assertNotEq(calculatedAddress3, address(moneyBox)); + + address calculatedAddress4 = exampleMoneyBoxUser.calculateMoneyBoxAddress(user2, salt, address(0xee00bb)); + assertNotEq(calculatedAddress4, address(moneyBox)); + + } + +} diff --git a/test/RealityETH.t.sol b/test/RealityETH.t.sol new file mode 100644 index 00000000..8ce5741a --- /dev/null +++ b/test/RealityETH.t.sol @@ -0,0 +1,195 @@ +pragma solidity ^0.8.20; + +/* +Tests for new features added to reality.eth while developing Subjectivocracy interaction. +Ultimately these will probably be moved to the reality.eth repo and included in a normal release +...unless it turns out that we need subjectivocracy-specific changes that we don't want in the normal version. +*/ + +/* 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 {RealityETH_v3_0} from "../contracts/lib/reality-eth/RealityETH-3.0.sol"; + +contract RealityETHTest is Test { + + Arbitrator internal arb; + RealityETH_v3_0 internal realityEth; + bytes32 internal q1; + bytes32 internal q2; + + address payable internal user1 = payable(address(0xbabe01)); + address payable internal user2 = payable(address(0xbabe02)); + + bytes32 constant internal BYTES32_YES = bytes32(uint256(1)); + bytes32 constant internal BYTES32_NO = bytes32(uint256(0)); + + bytes32[] internal historyHashes; + address[] internal addrs; + uint256[] internal bonds; + bytes32[] internal answers; + + // Store the history for the number of entries set in numEntries in historyHashes etc + function _logsToHistory(Vm.Log[] memory logs) internal { + + /* + Some features need us to send the contract the answer history. + This function will construct it from the logs in the order required. + event LogNewAnswer(bytes32 answer, bytes32 indexed question_id, bytes32 history_hash, address indexed user, uint256 bond, uint256 ts, bool is_commitment) + */ + + bytes32 logNewAnswerSignature = keccak256("LogNewAnswer(bytes32,bytes32,bytes32,address,uint256,uint256,bool)"); + + for(uint256 idx = logs.length; idx > 0; idx--) { + + uint256 i = idx - 1; + + // Skip any other log + if (logs[i].topics[0] != logNewAnswerSignature) { + continue; + } + + (bytes32 logAnswer, bytes32 logHistoryHash, uint256 logBond,,) = abi.decode(logs[i].data, (bytes32,bytes32,uint256,uint256,bool)); + address logUser = address(uint160(uint256(logs[i].topics[2]))); + + addrs.push(logUser); + bonds.push(logBond); + answers.push(logAnswer); + historyHashes.push(logHistoryHash); + + } + + // historyHashes is in the reverse order (highest bond to lowest), go forwards now + for(uint256 j = 0; j < historyHashes.length; j++) { + // For the final element there is no next one, it's empty + if (j < historyHashes.length-1) { + historyHashes[j] = historyHashes[j+1]; + } else { + historyHashes[j] = bytes32(0); + } + } + + } + + + function _trimLogs() internal { + + historyHashes.pop(); + addrs.pop(); + bonds.pop(); + answers.pop(); + } + + function setUp() public { + + realityEth = new RealityETH_v3_0(); + + arb = new Arbitrator(); + arb.setRealitio(address(realityEth)); + arb.setDisputeFee(50); + + user1.transfer(1000000); + user2.transfer(1000000); + + q1 = realityEth.askQuestion(0, "Question 1", address(arb), uint32(6000), 0, 0); + q2 = realityEth.askQuestion(0, "Question 2", address(arb), uint32(6000), 0, 0); + + vm.recordLogs(); + + vm.prank(user1); + realityEth.submitAnswer{value: 5}(q1, BYTES32_YES, 0); + + vm.prank(user2); + realityEth.submitAnswer{value: 25}(q1, BYTES32_NO, 0); + + vm.prank(user1); + realityEth.submitAnswer{value: 500}(q1, BYTES32_YES, 0); + + // Put an unrevealed commit at 1000 + uint256 nonce1 = uint256(555554321); + bytes32 answerHash1 = keccak256(abi.encodePacked(BYTES32_NO, nonce1)); + uint256 bond1 = 1000; + vm.prank(user1); + realityEth.submitAnswerCommitment{value: bond1}(q1, answerHash1, 0, user1); + + vm.prank(user2); + realityEth.submitAnswer{value: 2500}(q1, BYTES32_NO, 0); + + // We'll do this one as a commit-reveal + uint256 nonce2 = uint256(1232); + bytes32 answerHash2 = keccak256(abi.encodePacked(BYTES32_NO, nonce2)); + uint256 bond2 = 5000; + vm.prank(user2); + realityEth.submitAnswerCommitment{value: bond2}(q1, answerHash2, 0, user2); + realityEth.submitAnswerReveal(q1, BYTES32_NO, nonce2, bond2); + + // Do a commit-reveal for yes + uint256 nonce3 = uint256(9876); + bytes32 answerHash3 = keccak256(abi.encodePacked(BYTES32_YES, nonce3)); + uint256 bond3 = 10000; + vm.prank(user1); + realityEth.submitAnswerCommitment{value: bond3}(q1, answerHash3, 0, user1); + realityEth.submitAnswerReveal(q1, BYTES32_YES, nonce3, bond3); + + vm.prank(user1); + realityEth.submitAnswer{value: 20000}(q1, BYTES32_YES, 0); + + vm.prank(user2); + realityEth.submitAnswer{value: 40000}(q1, BYTES32_NO, 0); + + _logsToHistory(vm.getRecordedLogs()); + + } + + function _checkSuppliedHistory(bytes32 expectedAnswer, uint256 expectedBond) internal { + + (bytes32 finalAnswer, uint256 finalBond) = realityEth.getEarliestAnswerFromSuppliedHistoryOrRevert(q1, historyHashes, addrs, bonds, answers); + assertEq(finalAnswer, expectedAnswer); + assertEq(finalBond, expectedBond); + + } + + function _checkSuppliedHistoryUnrevealedCommit() internal { + + vm.expectRevert("Earliest answer is an unrevealed commitment"); + realityEth.getEarliestAnswerFromSuppliedHistoryOrRevert(q1, historyHashes, addrs, bonds, answers); + + } + + function testGetEarliestAnswerFromSuppliedHistoryOrRevert() public { + + _checkSuppliedHistory(BYTES32_YES, 5); + _trimLogs(); + _checkSuppliedHistory(BYTES32_NO, 25); + _trimLogs(); + _checkSuppliedHistory(BYTES32_YES, 500); + _trimLogs(); + _checkSuppliedHistoryUnrevealedCommit(); + _trimLogs(); + _checkSuppliedHistory(BYTES32_NO, 2500); + _trimLogs(); + _checkSuppliedHistory(BYTES32_NO, 5000); + _trimLogs(); + _checkSuppliedHistory(BYTES32_YES, 10000); + _trimLogs(); + _checkSuppliedHistory(BYTES32_YES, 20000); + _trimLogs(); + _checkSuppliedHistory(BYTES32_NO, 40000); + + } + + function testGetEarliestAnswerFromSuppliedHistoryOrRevertWrongHashReverts() public { + + // Make one of the history hashes wrong + historyHashes[2] = bytes32(0); + vm.expectRevert("History input provided did not match the expected hash"); + realityEth.getEarliestAnswerFromSuppliedHistoryOrRevert(q1, historyHashes, addrs, bonds, answers); + + } + +} diff --git a/test/testcontract/ExampleMoneyBoxUser.sol b/test/testcontract/ExampleMoneyBoxUser.sol new file mode 100644 index 00000000..75036e0b --- /dev/null +++ b/test/testcontract/ExampleMoneyBoxUser.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +import {CalculateMoneyBoxAddress} from "../../contracts/lib/CalculateMoneyBoxAddress.sol"; + +contract ExampleMoneyBoxUser { + + function calculateMoneyBoxAddress(address _creator, bytes32 _salt, address _token) external pure returns (address) { + return CalculateMoneyBoxAddress._calculateMoneyBoxAddress(_creator, _salt, _token); + } + +} diff --git a/test/testcontract/ExampleToken.sol b/test/testcontract/ExampleToken.sol new file mode 100644 index 00000000..77bc11d0 --- /dev/null +++ b/test/testcontract/ExampleToken.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract ExampleToken is ERC20 { + + constructor() ERC20("My Token", "MT") { + } + + // Permission-free mint for testing + function fakeMint(address _to, uint256 _amount) external { + _mint(_to, _amount); + } + +} diff --git a/test/testcontract/MockPolygonZkEVMBridge.sol b/test/testcontract/MockPolygonZkEVMBridge.sol new file mode 100644 index 00000000..f9938822 --- /dev/null +++ b/test/testcontract/MockPolygonZkEVMBridge.sol @@ -0,0 +1,42 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +import {IBridgeMessageReceiver} from "@RealityETH/zkevm-contracts/contracts/interfaces/IBridgeMessageReceiver.sol"; + +contract MockPolygonZkEVMBridge { + + function bridgeMessage( + uint32 destinationNetwork, + address destinationAddress, + bool forceUpdateGlobalExitRoot, + bytes calldata metadata + ) public virtual payable { + } + + function bridgeAsset( + uint32 destinationNetwork, + address destinationAddress, + uint256 amount, + address token, + bool forceUpdateGlobalExitRoot, + bytes calldata permitData + ) + public + payable { + } + + receive() external payable {} + + function fakeClaimMessage(address originAddress, uint32 originNetwork, address destinationAddress, bytes memory metadata, uint256 amount) external { + /* solhint-disable avoid-low-level-calls */ + (bool success, ) = destinationAddress.call{value: amount}( + abi.encodeCall( + IBridgeMessageReceiver.onMessageReceived, + (originAddress, originNetwork, metadata) + ) + ); + require(success, "Call failed"); + } + +}