diff --git a/contracts/ForkableRealityETH_ERC20.sol b/contracts/ForkableRealityETH_ERC20.sol index a1c5ace0..61edb3f5 100644 --- a/contracts/ForkableRealityETH_ERC20.sol +++ b/contracts/ForkableRealityETH_ERC20.sol @@ -2,977 +2,174 @@ pragma solidity ^0.8.20; -import "./lib/reality-eth/BalanceHolder_ERC20.sol"; - -import "./interfaces/IForkableRealityETH.sol"; - -contract ForkableRealityETH_ERC20 is BalanceHolder_ERC20 { - 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; - - // Proportion withheld when you claim an earlier bond. - uint256 constant BOND_CLAIM_FEE_PROPORTION = 10; // One 10th ie 10% - - bool is_frozen; - - event LogNewTemplate( - uint256 indexed template_id, - address indexed user, - string question_text - ); - - event LogNewQuestion( - bytes32 indexed question_id, - address indexed user, - uint256 template_id, - string question, - bytes32 indexed content_hash, - address arbitrator, - uint32 timeout, - uint32 opening_ts, - uint256 nonce, - uint256 created - ); - - event LogNewAnswer( - bytes32 answer, - bytes32 indexed question_id, - bytes32 history_hash, - address indexed user, - uint256 bond, - uint256 ts, - bool is_commitment - ); - - event LogNotifyOfArbitrationRequest( - bytes32 indexed question_id, - address indexed user - ); - - event LogFinalize(bytes32 indexed question_id, bytes32 indexed answer); - - event LogClaim( - bytes32 indexed question_id, - address indexed user, - uint256 amount - ); - - struct Question { - bytes32 content_hash; - address arbitrator; // We could do without this as we only allow the token to be arbitrator, but keep it for API compatibility - uint32 opening_ts; - uint32 timeout; - uint32 finalize_ts; - bool is_pending_arbitration; - uint256 cumulative_bonds; // Not in regular reality.eth - bytes32 best_answer; - bytes32 history_hash; - uint256 bond; - bool is_finalized; // Not in regular reality.eth - } - - // 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 = 2147483648; // Start well ahead of 1 to avoid clashes with the standard built-in templates - mapping(uint256 => uint256) public templates; - mapping(uint256 => bytes32) public template_hashes; - mapping(bytes32 => Question) public questions; - mapping(bytes32 => Claim) public question_claims; - - 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 statePendingArbitration(bytes32 question_id) { - require( - questions[question_id].is_pending_arbitration, - "question must be pending arbitration" - ); - _; - } - - 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" - ); - require(!is_frozen, "contract must not be pending frozen"); - 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" - ); - _; - } - - // State in the forkable version where the question can no longer be answered, but is not yet marked finalized. - // Traditional reality.eth doesn't have this state as things just finalize automatically as soon as they're ready. - modifier stateAwaitingFinalization(bytes32 question_id) { - require(!questions[question_id].is_finalized, "Already finalized"); - - uint32 finalize_ts = questions[question_id].finalize_ts; - - // The same checks as the traditional isFinalized() - require( - !questions[question_id].is_pending_arbitration, - "Pending arbitration, cannot finalize yet" - ); - require(finalize_ts > UNANSWERED, "Not answered, cannot finalize"); - require( - finalize_ts <= uint32(block.timestamp), - "Finalization time has not yet arrived" - ); - _; - } - - modifier stateFinalized(bytes32 question_id) { - require(isFinalized(question_id), "question must be finalized"); - _; - } - - modifier stateFrozen() { - require(is_frozen, "Contract must be frozen"); - _; - } - - modifier bondMustDouble(bytes32 question_id, uint256 tokens) { - require(tokens > 0, "bond must be positive"); - require( - tokens >= (questions[question_id].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" - ); - } +import {RealityETHFreezable_ERC20} from "@reality.eth/contracts/development/contracts/RealityETHFreezable_ERC20.sol"; +import {ForkableStructure} from "./mixin/ForkableStructure.sol"; + +import {IERC20} from "@reality.eth/contracts/development/contracts/IERC20.sol"; +import {IForkonomicToken} from "./interfaces/IForkonomicToken.sol"; + +import {IForkableRealityETH_ERC20} from "./interfaces/IForkableRealityETH_ERC20.sol"; +import {L1ForkArbitrator} from "./L1ForkArbitrator.sol"; + +/* +This is a forkable version of the Reality.eth contract for use on L1. +*/ + +contract ForkableRealityETH_ERC20 is + RealityETHFreezable_ERC20, + ForkableStructure +{ + // Asking questions is locked down the the forkmanager. + // This isn't strictly necessary but it reduces the attack surface. + // TODO: We might want to replace this with a governance contract owned by the forkmanager. + modifier permittedQuestionerOnly() override { + if (msg.sender != forkmanager) revert PermittedQuestionerOnly(); _; } - function init( - IERC20 _token, - address _parent_reality_eth, - bytes32 _question_id - ) public { - require(address(token) == address(0), "Can only be initialized once"); - createTemplate( - '{"title": "Should we add arbitrator %s to whitelist contract %s", "type": "bool"}' - ); - createTemplate( - '{"title": "Should we remove arbitrator %s from whitelist contract %s", "type": "bool"}' - ); - createTemplate( - '{"title": "Should switch to ForkManager %s", "type": "bool"}' - ); - token = _token; - if (_question_id != bytes32(0x0)) { - _importQuestion(_parent_reality_eth, _question_id); - } - } - - /// @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 - ) private stateAny 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 Ask a new question without a bounty and return the ID - /// @dev Template data is only stored in the event logs, but its block number is kept in contract storage. - /// @dev Calling without the token param will only work if there is no arbitrator-set question fee. - /// @dev This has the same function signature as askQuestion() in the non-ERC20 version, which is optionally payable. - /// @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 - ) - public - returns ( - // stateNotCreated is enforced by the internal _askQuestion - 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, - msg.sender, - nonce + // This arbitrator is assigned automatically when importing questions + address public l1ForkArbitrator; + + uint256 constant UPGRADE_TEMPLATE_ID = 1048576; + + function initialize( + address _forkmanager, + address _parentContract, + address _token, + bytes32 _questionIdWeForkedOver + ) public initializer { + // We do this with a new contract instead of a proxy as it's pretty tiny + // TODO: Should we use a proxy pattern like elsewhere? + l1ForkArbitrator = address( + new L1ForkArbitrator( + address(this), + address(_forkmanager), + address(_token) ) ); - _askQuestion( - question_id, - content_hash, - arbitrator, - timeout, - opening_ts - ); - emit LogNewQuestion( - question_id, - msg.sender, - template_id, - question, - content_hash, - arbitrator, - timeout, - opening_ts, - nonce, - block.timestamp - ); - - return question_id; - } - - function _deductTokensOrRevert(uint256 tokens) internal { - if (tokens == 0) { - return; - } + ForkableStructure.initialize(_forkmanager, _parentContract); - uint256 bal = balanceOf[msg.sender]; + _createInitialTemplates(); + token = IERC20(_token); - // Deduct any tokens you have in your internal balance first - if (bal > 0) { - if (bal >= tokens) { - balanceOf[msg.sender] = bal - tokens; - return; - } else { - tokens = tokens - bal; - balanceOf[msg.sender] = 0; - } + // We immediately import the initial question we forked over, which keep its original arbitrator. + // (Any other imported question will use the new l1ForkArbitrator) + if (_questionIdWeForkedOver != bytes32(0x0)) { + address parentArbitrator = ForkableRealityETH_ERC20(parentContract) + .l1ForkArbitrator(); + _importQuestion(_questionIdWeForkedOver, parentArbitrator); } - // Now we need to charge the rest from - require( - token.transferFrom(msg.sender, address(this), tokens), - "Transfer of tokens failed, insufficient approved balance?" - ); - return; - } - - function _askQuestion( - bytes32 question_id, - bytes32 content_hash, - address arbitrator, - uint32 timeout, - uint32 opening_ts - ) internal stateNotCreated(question_id) { - // 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"); - require( - arbitrator == address(token), - "Our ForkManager must be the arbitrator" - ); - require( - arbitrator == msg.sender, - "Questions must be proxied through the ForkManager" - ); - - questions[question_id].content_hash = content_hash; - questions[question_id].arbitrator = arbitrator; - questions[question_id].opening_ts = opening_ts; - questions[question_id].timeout = timeout; } - /// @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. - /// @param tokens The amount of tokens to submit - function submitAnswerERC20( - bytes32 question_id, - bytes32 answer, - uint256 max_previous, - uint256 tokens - ) - external - stateOpen(question_id) - bondMustDouble(question_id, tokens) - previousBondMustNotBeatMaxPrevious(question_id, max_previous) - { - _deductTokensOrRevert(tokens); - _addAnswerToHistory(question_id, answer, msg.sender, tokens); - _updateCurrentAnswer(question_id, answer); - } + function _createInitialTemplates() internal override { + // TODO: Decide if we want to include the original templates for consistency/flexibility + // ...even though they won't be used unless we upgrade the forkmanager or whatever governs this. - /// @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 - /// @param tokens The amount of tokens to submit - function submitAnswerForERC20( - bytes32 question_id, - bytes32 answer, - uint256 max_previous, - address answerer, - uint256 tokens - ) - external - payable - stateOpen(question_id) - bondMustDouble(question_id, tokens) - previousBondMustNotBeatMaxPrevious(question_id, max_previous) - { - _deductTokensOrRevert(tokens); - require(answerer != NULL_ADDRESS, "answerer must be non-zero"); - _addAnswerToHistory(question_id, answer, answerer, tokens); - _updateCurrentAnswer(question_id, answer); + // Bump the next template ID to stay clear of the range of the normal stock templates, as this might confuse the UI + nextTemplateID = UPGRADE_TEMPLATE_ID; + createTemplate( + '{"title": "Should we execute the upgrade contract %s?", "type": "bool"}' + ); } - function _addAnswerToHistory( - bytes32 question_id, - bytes32 answer_or_commitment_id, - address answerer, - uint256 bond + /// Copies a question from an old instance to this new one after a fork + /// The budget for this question should have been transferred when we did the fork. + /// We won't delete the question on the parent reality.eth contract, but you won't be able to do anything with it because it'll be frozen. + /// NB The question ID will no longer match the hash of the content, as the arbitrator has changed + /// @param _questionId - The ID of the question to import + /// @param _newArbitrator - The new arbitrator we should use. + function _importQuestion( + bytes32 _questionId, + address _newArbitrator ) internal { - require( - answer_or_commitment_id == bytes32(uint256(1)) || - answer_or_commitment_id == bytes32(uint256(0)), - "Only answer 1 and 0 are supported" - ); - - bytes32 new_history_hash = keccak256( - abi.encodePacked( - questions[question_id].history_hash, - answer_or_commitment_id, - bond, - answerer, - false - ) + IForkableRealityETH_ERC20 parent = IForkableRealityETH_ERC20( + parentContract ); + uint32 timeout = parent.getTimeout(_questionId); + uint32 finalizeTS = parent.getFinalizeTS(_questionId); + bool isPendingArbitration = parent.isPendingArbitration(_questionId); - // 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].cumulative_bonds = - questions[question_id].cumulative_bonds + - bond; + // For any open question, bump the finalization time to the import time plus a normal timeout period. + if ( + finalizeTS > 0 && + !isPendingArbitration && + !parent.isFinalized(_questionId) + ) { + finalizeTS = uint32(block.timestamp + timeout); } - questions[question_id].history_hash = new_history_hash; - emit LogNewAnswer( - answer_or_commitment_id, - question_id, - new_history_hash, - answerer, - bond, - block.timestamp, - false + questions[_questionId] = Question( + parent.getContentHash(_questionId), + _newArbitrator, + parent.getOpeningTS(_questionId), + timeout, + finalizeTS, + isPendingArbitration, + parent.getBounty(_questionId), + parent.getBestAnswer(_questionId), + parent.getHistoryHash(_questionId), + parent.getBond(_questionId), + parent.getMinBond(_questionId) ); } - 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; - } - - function _updateCurrentAnswerByArbitrator( - bytes32 question_id, - bytes32 answer - ) internal { - questions[question_id].best_answer = answer; - questions[question_id].finalize_ts = uint32(block.timestamp); + // Anyone can import any question if it has not already been imported. + function importQuestion( + bytes32 questionId + ) external stateNotCreated(questionId) { + _importQuestion(questionId, l1ForkArbitrator); } - /// @notice Notify the contract that the arbitrator has been paid for a question - /// @dev Normally this would freeze the question, but in the Forkable case we migrate the question to forks, cancel its answers, and freeze everything - /// @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 - ) + function createChildren() external - onlyArbitrator(question_id) - stateOpen(question_id) - previousBondMustNotBeatMaxPrevious(question_id, max_previous) + onlyForkManger + returns (address, address) { - require( - questions[question_id].bond > 0, - "Question must already have an answer when arbitration is requested" - ); - questions[question_id].is_pending_arbitration = true; - require( - msg.sender == address(token), - "Only the owner (ForkManger token) can do freeze the contract" - ); - - is_frozen = true; - - // We will never actually send the arbitration result to this contract, only to the forks. - // This means the funds submitted for this question are effectively frozen. - // They will instead be credited to the child questions in the forked tokens.. - - emit LogNotifyOfArbitrationRequest(question_id, requester); - } - - /// @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 - ) public onlyArbitrator(question_id) statePendingArbitration(question_id) { - 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); - _updateCurrentAnswerByArbitrator(question_id, answer); - questions[question_id].is_finalized = true; - emit LogFinalize(question_id, answer); - } - - /// @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) public view returns (bool) { - return questions[question_id].is_finalized; - } - - /// @notice Report whether the answer to the specified question is finalized - /// @dev Returns false if pending arbitration - /// @param question_id The ID of the question - /// @return Return true if finalized - function canBeFinalized(bytes32 question_id) public view returns (bool) { - if (is_frozen) { - return false; - } - uint32 finalize_ts = questions[question_id].finalize_ts; - return (!questions[question_id].is_pending_arbitration && - (finalize_ts > UNANSWERED) && - (finalize_ts <= uint32(block.timestamp))); - } - - /// @notice Finalize the question - /// @dev This isn't done in the unforkable version, there we just finalize by time. - /// @param question_id The ID of the question - function finalize( - bytes32 question_id - ) external stateAwaitingFinalization(question_id) { - require(canBeFinalized(question_id), "Cannot be finalized yet"); - questions[question_id].is_finalized = true; - emit LogFinalize(question_id, questions[question_id].best_answer); - } - - /// @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 - ) external view stateFinalized(question_id) 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 - ) external view stateFinalized(question_id) returns (bytes32) { - return questions[question_id].best_answer; - } - - function _subtractClaimFee( - bytes32 question_id, - uint256 last_bond - ) internal returns (uint256) { - address arbitrator = questions[question_id].arbitrator; - // Give the arbitrator 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) { - uint256 claim_fee = last_bond / BOND_CLAIM_FEE_PROPORTION; - last_bond = last_bond - claim_fee; - balanceOf[arbitrator] = balanceOf[arbitrator] + claim_fee; - } - return last_bond; + return _createChildren(); } - /// @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 - ) external view stateFinalized(question_id) returns (bytes32) { - require( - content_hash == questions[question_id].content_hash, - "content hash must match" + // Move our internal balance record for a single address to the child contracts after a fork. + function moveBalanceToChildren( + address beneficiary + ) external onlyAfterForking { + uint256 bal = balanceOf[beneficiary]; + balanceOf[beneficiary] = 0; + IForkableRealityETH_ERC20(children[0]).creditBalanceFromParent( + beneficiary, + bal ); - require( - arbitrator == questions[question_id].arbitrator, - "arbitrator must match" + IForkableRealityETH_ERC20(children[1]).creditBalanceFromParent( + beneficiary, + bal ); - 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 - ) public stateFinalized(question_id) { - 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. - _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] - ); - - // Line the bond up for next time, when it will be added to somebody's queued_funds - last_bond = _subtractClaimFee(question_id, bonds[i]); - 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 creditBalanceFromParent( + address beneficiary, + uint256 amount + ) external onlyParent { + balanceOf[beneficiary] = balanceOf[beneficiary] + amount; } - function _payPayee( - bytes32 question_id, - address payee, - uint256 value + function _moveTokensToChild( + address _childRealityETH, + uint256 amount ) internal { - balanceOf[payee] = balanceOf[payee] + value; - emit LogClaim(question_id, payee, value); - } - - 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 - ) 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 (answer == best_answer) { - if (payee == NULL_ADDRESS) { - // The entry is for the first payee we come to, ie the winner. - payee = addr; - } 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 - ) - public - stateAny // The finalization checks are done in the claimWinnings function - { - 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 Refunds everyone who took part. - /// Caller must provide the answer history, in reverse order, like when claiming winnings. - /// 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 refund( - bytes32 question_id, - bytes32[] memory history_hashes, - address[] memory addrs, - uint256[] memory bonds, - bytes32[] memory answers - ) public stateFrozen stateOpen(question_id) { - require( - history_hashes.length > 0, - "at least one history hash entry must be provided" - ); - - uint256 i; - for (i = 0; i < history_hashes.length; i++) { - // Check input against the history hash - _verifyHistoryInputOrRevert( - questions[question_id].history_hash, - history_hashes[i], - answers[i], - bonds[i], - addrs[i] - ); - - balanceOf[addrs[i]] = balanceOf[addrs[i]] + bonds[i]; - questions[question_id].history_hash = history_hashes[i]; - } - } - - /// @notice Convenience function to refund 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 refundMultipleAndWithdrawBalance( - bytes32[] memory question_ids, - uint256[] memory lengths, - bytes32[] memory hist_hashes, - address[] memory addrs, - uint256[] memory bonds, - bytes32[] memory answers - ) public stateFrozen { - 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++; - } - refund(qid, hh, ad, bo, an); - } - withdraw(); - } - - /// Copies a question from an old instance to this new one after a fork - /// The question we fork over, which will be the only one pending arbitration, will be migrated with its answers. - /// The budget for this question should have been transferred when we did the fork. - /// Other questions will to be created as if asked new, but this method preserves their old question_ids - /// NB The question_id will no longer match the hash of the content, as the arbitrator has changed - /// @param _parent - The address of the reality.eth we should import from - /// @param _question_id - The ID of the question to migrate. - function _importQuestion(address _parent, bytes32 _question_id) internal { - IForkableRealityETH parent = IForkableRealityETH(_parent); - questions[_question_id] = Question( - parent.getContentHash(_question_id), - msg.sender, - parent.getOpeningTS(_question_id), - parent.getTimeout(_question_id), - parent.getFinalizeTS(_question_id), - true, - parent.getCumulativeBonds(_question_id), - parent.getBestAnswer(_question_id), - parent.getHistoryHash(_question_id), - parent.getBond(_question_id), - false + address childToken = address( + IForkableRealityETH_ERC20(_childRealityETH).token() ); + IForkonomicToken(childToken).transfer(_childRealityETH, amount); } - /// @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 cumulative bonds - /// @dev Used when migrating questions in forks, as we have to give the child contract the money to pay out - /// @param question_id The ID of the question - function getCumulativeBonds( - bytes32 question_id - ) public view returns (uint256) { - return questions[question_id].cumulative_bonds; - } - - /// @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; + function handleInitiateFork() external onlyForkManger { + freezeTs = uint32(block.timestamp); } - /// @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; + function handleExecuteFork() external onlyAfterForking { + uint256 balance = token.balanceOf(address(this)); + IForkonomicToken(address(token)).splitTokensIntoChildTokens(balance); + _moveTokensToChild(children[0], balance); + _moveTokensToChild(children[1], balance); } } diff --git a/contracts/L1ForkArbitrator.sol b/contracts/L1ForkArbitrator.sol new file mode 100644 index 00000000..9babe8c1 --- /dev/null +++ b/contracts/L1ForkArbitrator.sol @@ -0,0 +1,121 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +/* +This is a very limited Arbitrator contract for the purpose of handling an arbitration request and passing it on to the ForkingManager. +It doesn't handle submitting the answer to the winning question, this is instead handled by the child reality.eth contract when forking. +*/ + +import {IRealityETH} from "@reality.eth/contracts/development/contracts/IRealityETH.sol"; +import {IArbitratorCore} from "@reality.eth/contracts/development/contracts/IArbitratorCore.sol"; +import {IArbitratorErrors} from "@reality.eth/contracts/development/contracts/IArbitratorErrors.sol"; +import {IForkingManager} from "./interfaces/IForkingManager.sol"; +import {IForkonomicToken} from "./interfaces/IForkonomicToken.sol"; +import {IForkableStructure} from "./interfaces/IForkableStructure.sol"; + +contract L1ForkArbitrator is IArbitratorCore, IArbitratorErrors { + IForkingManager public forkmanager; + IRealityETH public realitio; + IForkonomicToken public token; + + /// @notice Could not approve the transfer for some reason + error CouldNotApproveTransfer(); + + /// @notice Could not deduct fee + error CouldNotDeductFee(); + + /// @notice Not forked yet + error NotForkedYet(); + + /// @notice This arbitrator can only arbitrate one dispute in its lifetime + error CanOnlyArbitrateOneDispute(); + + /// @notice The question ID must be supplied + error MissingQuestionID(); + + bytes32 public arbitratingQuestionId; + address public payer; + + constructor(address _realitio, address _forkmanager, address _token) { + realitio = IRealityETH(_realitio); + forkmanager = IForkingManager(_forkmanager); + token = IForkonomicToken(_token); + } + + /* solhint-disable quotes */ + // Tells the UI to know that it should prompt the user to use ERC20 instead of the native token. + string public metadata = '{"erc20": 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) { + return forkmanager.arbitrationFee(); + } + + /// @notice Request arbitration, freezing the question until we send submitAnswerByArbitrator + /// @dev Requires payment in token(), which the UI as of early 2024 will not handle. + /// @param _questionId The question in question + /// @param _maxPrevious If specified, reverts if a bond higher than this was submitted after you sent your transaction. + /// NB TODO: This is payable because the interface expects native tokens. Consider changing the interface or adding an ERC20 version. + function requestArbitration( + bytes32 _questionId, + uint256 _maxPrevious + ) external payable returns (bool) { + if (arbitratingQuestionId != bytes32(0)) + revert CanOnlyArbitrateOneDispute(); + if (_questionId == bytes32(0)) revert MissingQuestionID(); + + uint256 fee = getDisputeFee(_questionId); + if (fee == 0) + revert TheArbitratorMustHaveSetANonZeroFeeForTheQuestion(); + + // First we transfer the fee to ourselves. + if (!token.transferFrom(msg.sender, address(this), fee)) + revert CouldNotDeductFee(); + + payer = msg.sender; + arbitratingQuestionId = _questionId; + + realitio.notifyOfArbitrationRequest( + _questionId, + msg.sender, + _maxPrevious + ); + + // Now approve so that the fee can be transferred right out to the ForkingManager + if (!token.approve(address(forkmanager), fee)) + revert CouldNotApproveTransfer(); + IForkingManager.DisputeData memory disputeData = IForkingManager + .DisputeData(true, address(this), _questionId); + IForkingManager(forkmanager).initiateFork(disputeData); + + return true; + } + + function settleChildren( + bytes32 lastHistoryHash, + bytes32 lastAnswer, + address lastAnswerer + ) public { + (address child1, address child2) = IForkableStructure(address(realitio)) + .getChildren(); + if (child1 == address(0) || child2 == address(0)) revert NotForkedYet(); + IRealityETH(child1).assignWinnerAndSubmitAnswerByArbitrator( + arbitratingQuestionId, + bytes32(uint256(1)), + payer, + lastHistoryHash, + lastAnswer, + lastAnswerer + ); + IRealityETH(child2).assignWinnerAndSubmitAnswerByArbitrator( + arbitratingQuestionId, + bytes32(uint256(0)), + payer, + lastHistoryHash, + lastAnswer, + lastAnswerer + ); + } +} diff --git a/contracts/interfaces/IForkableRealityETH.sol b/contracts/interfaces/IForkableRealityETH.sol deleted file mode 100644 index 976b3e12..00000000 --- a/contracts/interfaces/IForkableRealityETH.sol +++ /dev/null @@ -1,128 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0-only - -pragma solidity ^0.8.10; - -interface IForkableRealityETH { - function claimWinnings( - bytes32 question_id, - bytes32[] memory history_hashes, - address[] memory addrs, - uint256[] memory bonds, - bytes32[] memory answers - ) external; - - function getFinalAnswerIfMatches( - bytes32 question_id, - bytes32 content_hash, - address arbitrator, - uint32 min_timeout, - uint256 min_bond - ) external view returns (bytes32); - - function getArbitrator(bytes32 question_id) external view returns (address); - - function getBond(bytes32 question_id) external view returns (uint256); - - function claimMultipleAndWithdrawBalance( - bytes32[] memory question_ids, - uint256[] memory lengths, - bytes32[] memory hist_hashes, - address[] memory addrs, - uint256[] memory bonds, - bytes32[] memory answers - ) external; - - function withdraw() external; - - function template_hashes(uint256) external view returns (bytes32); - - function getContentHash( - bytes32 question_id - ) external view returns (bytes32); - - function balanceOf(address) external view returns (uint256); - - function askQuestion( - uint256 template_id, - string memory question, - address arbitrator, - uint32 timeout, - uint32 opening_ts, - uint256 nonce - ) external payable returns (bytes32); - - function submitAnswer( - bytes32 question_id, - bytes32 answer, - uint256 max_previous - ) external payable; - - function isFinalized(bytes32 question_id) external view returns (bool); - - function isPendingArbitration( - bytes32 question_id - ) external view returns (bool); - - function getHistoryHash( - bytes32 question_id - ) external view returns (bytes32); - - function getBestAnswer(bytes32 question_id) external view returns (bytes32); - - function questions( - bytes32 - ) - external - view - returns ( - 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 - ); - - function getOpeningTS(bytes32 question_id) external view returns (uint32); - - function getTimeout(bytes32 question_id) external view returns (uint32); - - function getFinalAnswer( - bytes32 question_id - ) external view returns (bytes32); - - function getFinalizeTS(bytes32 question_id) external view returns (uint32); - - function templates(uint256) external view returns (uint256); - - function resultFor(bytes32 question_id) external view returns (bytes32); - - function notifyOfArbitrationRequest( - bytes32 question_id, - address requester, - uint256 max_previous - ) external; - - function submitAnswerByArbitrator( - bytes32 question_id, - bytes32 answer, - address answerer - ) external; - - 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; - - function getCumulativeBonds( - bytes32 question_id - ) external view returns (uint256); -} diff --git a/contracts/interfaces/IForkableRealityETH_ERC20.sol b/contracts/interfaces/IForkableRealityETH_ERC20.sol new file mode 100644 index 00000000..35b7aaae --- /dev/null +++ b/contracts/interfaces/IForkableRealityETH_ERC20.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: GPL-3.0-only + +pragma solidity ^0.8.20; + +import {IRealityETH_ERC20} from "@reality.eth/contracts/development/contracts/IRealityETH_ERC20.sol"; + +// solhint-disable-next-line contract-name-camelcase +interface IForkableRealityETH_ERC20 is IRealityETH_ERC20 { + function creditBalanceFromParent( + address beneficiary, + uint256 amount + ) external; + function l1ForkArbitrator() external returns (address); +} diff --git a/contracts/interfaces/IForkingManager.sol b/contracts/interfaces/IForkingManager.sol index 2332f688..fb69f8de 100644 --- a/contracts/interfaces/IForkingManager.sol +++ b/contracts/interfaces/IForkingManager.sol @@ -36,7 +36,7 @@ interface IForkingManager is IForkableStructure { function globalExitRoot() external returns (address); - function arbitrationFee() external returns (uint256); + function arbitrationFee() external view returns (uint256); function disputeData() external diff --git a/package-lock.json b/package-lock.json index 8500047c..cbc648bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "@openzeppelin/contracts": "^4.9.5", "@openzeppelin/contracts-upgradeable": "^4.9.5", "@openzeppelin/hardhat-upgrades": "1.22.1", + "@reality.eth/contracts": "4.0.0-rc.5", "@RealityETH/zkevm-contracts": "github:RealityETH/zkevm-contracts#a090458140cdfce23763af887ff0767469368923", "@types/sinon-chai": "^3.2.3", "circomlibjs": "0.1.1", @@ -2277,6 +2278,15 @@ "prettier": "^3.0.0" } }, + "node_modules/@reality.eth/contracts": { + "version": "4.0.0-rc.5", + "resolved": "https://registry.npmjs.org/@reality.eth/contracts/-/contracts-4.0.0-rc.5.tgz", + "integrity": "sha512-QXpI4L/pagwrvgLXWMKgaYvzNFp6ImDMJWqH5GcCadyLXFVwdU0N0IhbgAdGUTqHZ+CdCMWWf392V0YhyLZisg==", + "dev": true, + "dependencies": { + "ethers": "^5.6.8" + } + }, "node_modules/@RealityETH/zkevm-contracts": { "name": "@0xpolygonhermez/zkevm-contracts", "version": "3.0.0", @@ -14182,6 +14192,15 @@ "dev": true, "requires": {} }, + "@reality.eth/contracts": { + "version": "4.0.0-rc.5", + "resolved": "https://registry.npmjs.org/@reality.eth/contracts/-/contracts-4.0.0-rc.5.tgz", + "integrity": "sha512-QXpI4L/pagwrvgLXWMKgaYvzNFp6ImDMJWqH5GcCadyLXFVwdU0N0IhbgAdGUTqHZ+CdCMWWf392V0YhyLZisg==", + "dev": true, + "requires": { + "ethers": "^5.6.8" + } + }, "@RealityETH/zkevm-contracts": { "version": "git+ssh://git@github.com/RealityETH/zkevm-contracts.git#a090458140cdfce23763af887ff0767469368923", "integrity": "sha512-T/ljSvXriV/1o+CtlQMXD4vIzqlEM2rUJqa6qW4nEsViu+1z7BSLgwF65tGRLv4yIIlRiFyTPA3nPvfRaABMgA==", diff --git a/package.json b/package.json index 6d1fbe8b..925bc736 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@openzeppelin/contracts": "^4.9.5", "@openzeppelin/contracts-upgradeable": "^4.9.5", "@openzeppelin/hardhat-upgrades": "1.22.1", + "@reality.eth/contracts": "4.0.0-rc.5", "@RealityETH/zkevm-contracts": "github:RealityETH/zkevm-contracts#a090458140cdfce23763af887ff0767469368923", "@types/sinon-chai": "^3.2.3", "circomlibjs": "0.1.1", diff --git a/remappings.txt b/remappings.txt index 1076a425..845bd0af 100644 --- a/remappings.txt +++ b/remappings.txt @@ -1,2 +1,2 @@ ds-test/=lib/forge-std/lib/ds-test/src/ -forge-std/=lib/forge-std/src/ \ No newline at end of file +forge-std/=lib/forge-std/src/ diff --git a/test/AdjudicationFramework/AdjudicationFrameworkMinimal.t.sol b/test/AdjudicationFramework/AdjudicationFrameworkMinimal.t.sol index 0aa19771..2dc1e442 100644 --- a/test/AdjudicationFramework/AdjudicationFrameworkMinimal.t.sol +++ b/test/AdjudicationFramework/AdjudicationFrameworkMinimal.t.sol @@ -128,7 +128,12 @@ contract AdjudicationIntegrationTest is Test { ); l1RealityEth = new ForkableRealityETH_ERC20(); - l1RealityEth.init(tokenMock, address(0), bytes32(0)); + l1RealityEth.initialize( + address(l1ForkingManager), + address(0), + address(tokenMock), + bytes32(0) + ); /* Creates templates 1, 2, 3 as diff --git a/test/AdjudicationFramework/AdjudicationFrameworkRequests.t.sol b/test/AdjudicationFramework/AdjudicationFrameworkRequests.t.sol index c661c09c..6c64bb00 100644 --- a/test/AdjudicationFramework/AdjudicationFrameworkRequests.t.sol +++ b/test/AdjudicationFramework/AdjudicationFrameworkRequests.t.sol @@ -127,7 +127,12 @@ contract AdjudicationIntegrationTest is Test { ); l1RealityEth = new ForkableRealityETH_ERC20(); - l1RealityEth.init(tokenMock, address(0), bytes32(0)); + l1RealityEth.initialize( + address(l1ForkingManager), + address(0), + address(tokenMock), + (0) + ); /* Creates templates 1, 2, 3 as diff --git a/test/ForkableRealityETH.t.sol b/test/ForkableRealityETH.t.sol new file mode 100644 index 00000000..2834eef8 --- /dev/null +++ b/test/ForkableRealityETH.t.sol @@ -0,0 +1,1417 @@ +pragma solidity ^0.8.20; + +import {Test} from "forge-std/Test.sol"; +import {IRealityETHErrors} from "@reality.eth/contracts/development/contracts/IRealityETHErrors.sol"; +import {ForkableRealityETH_ERC20} from "../contracts/ForkableRealityETH_ERC20.sol"; +import {ForkonomicToken} from "../contracts/ForkonomicToken.sol"; +import {IForkableStructure} from "../contracts/interfaces/IForkableStructure.sol"; +import {IForkonomicToken} from "../contracts/interfaces/IForkonomicToken.sol"; +import {IERC20Upgradeable} from "@openzeppelin/contracts-upgradeable/token/ERC20/IERC20Upgradeable.sol"; +import {IPolygonZkEVMBridge} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMBridge.sol"; +import {IPolygonZkEVMGlobalExitRoot} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVMGlobalExitRoot.sol"; +import {IVerifierRollup} from "@RealityETH/zkevm-contracts/contracts/interfaces/IVerifierRollup.sol"; +import {L1ForkArbitrator} from "../contracts/L1ForkArbitrator.sol"; +import {ForkingManager} from "../contracts/ForkingManager.sol"; +import {ChainIdManager} from "../contracts/ChainIdManager.sol"; +import {ForkableBridge} from "../contracts/ForkableBridge.sol"; +import {ForkableZkEVM} from "../contracts/ForkableZkEVM.sol"; +import {ForkableGlobalExitRoot} from "../contracts/ForkableGlobalExitRoot.sol"; +import {IPolygonZkEVM} from "@RealityETH/zkevm-contracts/contracts/interfaces/IPolygonZkEVM.sol"; + +// import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +contract ForkableRealityETHTest is Test { + // IERC20Upgradeable public token = IERC20Upgradeable(address(0x987654)); + + address public forkmanager; + address public forkmanager1 = address(0xabc1); + address public forkmanager2 = address(0xabc2); + address public forkmanager2a = address(0xabc2a); + address public forkmanager2b = address(0xabc2b); + + address public parentContract = address(0); + address public minter = address(0x789); + + address public forkonomicTokenImplementation; + address public forkonomicToken; + + address public forkableRealityETHImplementation; + address public forkableRealityETH; + + address public admin = address(0xad); + + bytes32 public forkOverQuestionId; + bytes32 public importFinalizedUnclaimedQuestionId; + bytes32 public importFinalizedClaimedQuestionId; + bytes32 public importUnansweredQuestionId; + bytes32 public importAnsweredQuestionId; + + address public answerGuyYes1 = address(0xbb0); + address public answerGuyYes2 = address(0xbb1); + address public answerGuyNo1 = address(0xbb2); + address public answerGuyNo2 = address(0xbb3); + uint256 public answerGuyMintAmount = 1000000; + bytes32[] public historyHashes; + + address public forkRequester = address(0xc01); + uint256 public arbitrationFee = 9999999; + uint64 public initialChainId = 1; + + uint256 public bond1 = 10000; + uint256 public bond2 = 20000; + uint256 public bond3 = 40000; + + bytes32 public answer1 = bytes32(uint256(1)); + bytes32 public answer2 = bytes32(uint256(0)); + bytes32 public answer3 = bytes32(uint256(1)); + + uint256 public constant UPGRADE_TEMPLATE_ID = 1048576; + + bytes32[32] public depositTree; + address public hardAssetManger = + address(0x1234567890123456789012345678901234567891); + uint32 public networkID = 10; + + uint64 public forkID = 3; + uint64 public pendingStateTimeout = 123; + uint64 public trustedAggregatorTimeout = 124235; + address public trustedSequencer = + address(0x1234567890123456789012345678901234567899); + address public trustedAggregator = + address(0x1234567890123456789012345678901234567898); + + function _initializeZKEVM( + address _zkevm, + uint64 _chainId, + address _bridge, + address _globalExitRoot + ) public { + IPolygonZkEVM.InitializePackedParameters + memory initializePackedParameters = IPolygonZkEVM + .InitializePackedParameters({ + admin: admin, + trustedSequencer: trustedSequencer, + pendingStateTimeout: pendingStateTimeout, + trustedAggregator: trustedAggregator, + trustedAggregatorTimeout: trustedAggregatorTimeout, + chainID: _chainId, + forkID: forkID + }); + ForkableZkEVM(_zkevm).initialize( + address(forkmanager), + address(0x0), + initializePackedParameters, + bytes32( + 0x827a9240c96ccb855e4943cc9bc49a50b1e91ba087007441a1ae5f9df8d1c57c + ), + "trustedSequencerURL", + "test network", + "0.0.1", + IPolygonZkEVMGlobalExitRoot(_globalExitRoot), + IERC20Upgradeable(address(forkonomicToken)), + IVerifierRollup(0x1234567890123456789012345678901234567893), + IPolygonZkEVMBridge(address(_bridge)) + ); + } + + function setUp() public { + address bridgeImplementation = address(new ForkableBridge()); + address bridge = address( + new TransparentUpgradeableProxy(bridgeImplementation, admin, "") + ); + + address forkmanagerImplementation = address(new ForkingManager()); + forkmanager = address( + new TransparentUpgradeableProxy( + forkmanagerImplementation, + admin, + "" + ) + ); + forkonomicTokenImplementation = address(new ForkonomicToken()); + forkonomicToken = address( + new TransparentUpgradeableProxy( + forkonomicTokenImplementation, + admin, + "" + ) + ); + + address zkevmImplementation = address(new ForkableZkEVM()); + address zkevm = address( + new TransparentUpgradeableProxy(zkevmImplementation, admin, "") + ); + + ForkonomicToken(forkonomicToken).initialize( + forkmanager, + parentContract, + minter, + "ForkonomicToken", + "FTK" + ); + address globalExitRootImplementation = address( + new ForkableGlobalExitRoot() + ); + address globalExitRoot = address( + new TransparentUpgradeableProxy( + globalExitRootImplementation, + admin, + "" + ) + ); + ForkableGlobalExitRoot(globalExitRoot).initialize( + address(forkmanager), + address(0x0), + address(zkevm), + bridge, + bytes32(0), + bytes32(0) + ); + ForkableBridge(bridge).initialize( + address(forkmanager), + address(0x0), + networkID, + ForkableGlobalExitRoot(globalExitRoot), + address(zkevm), + address(forkonomicToken), + false, + hardAssetManger, + 0, + depositTree + ); + address chainIdManager = address(new ChainIdManager(initialChainId)); + + _initializeZKEVM( + zkevm, + ChainIdManager(chainIdManager).getNextUsableChainId(), + bridge, + globalExitRoot + ); + + ForkingManager(forkmanager).initialize( + address(zkevm), + address(bridge), + address(forkonomicToken), + address(0x0), + address(globalExitRoot), + arbitrationFee, + chainIdManager, + uint256(60) + ); + + vm.prank(minter); + IForkonomicToken(forkonomicToken).mint(forkRequester, arbitrationFee); + + forkableRealityETHImplementation = address( + new ForkableRealityETH_ERC20() + ); + forkableRealityETH = address( + ForkableRealityETH_ERC20( + address( + new TransparentUpgradeableProxy( + forkableRealityETHImplementation, + admin, + "" + ) + ) + ) + ); + ForkableRealityETH_ERC20(forkableRealityETH).initialize( + forkmanager, + address(0), + forkonomicToken, + bytes32(0) + ); + + _setupAnswererBalances(); + _setupInitialQuestions(); + _setupHistoryHashes(); + } + + function _setupAnswererBalances() public { + vm.prank(minter); + IForkonomicToken(forkonomicToken).mint( + answerGuyYes1, + answerGuyMintAmount + ); + vm.prank(minter); + IForkonomicToken(forkonomicToken).mint( + answerGuyYes2, + answerGuyMintAmount + ); + vm.prank(minter); + IForkonomicToken(forkonomicToken).mint( + answerGuyNo1, + answerGuyMintAmount + ); + vm.prank(minter); + IForkonomicToken(forkonomicToken).mint( + answerGuyNo2, + answerGuyMintAmount + ); + } + + function _setupInitialQuestions() public { + address l1ForkArbitrator = ForkableRealityETH_ERC20(forkableRealityETH) + .l1ForkArbitrator(); + + vm.prank(forkmanager); + forkOverQuestionId = ForkableRealityETH_ERC20(forkableRealityETH) + .askQuestion( + UPGRADE_TEMPLATE_ID, + "import me", + l1ForkArbitrator, + 60, + 0, + 0 + ); + _setupInitialAnswers(forkOverQuestionId); + + // Just a 1 second window so we can finalize this one without finalizing the others + vm.prank(forkmanager); + importFinalizedUnclaimedQuestionId = ForkableRealityETH_ERC20( + forkableRealityETH + ).askQuestion( + UPGRADE_TEMPLATE_ID, + "finalize me but do not claim", + l1ForkArbitrator, + 1, + 0, + 0 + ); + _setupInitialAnswers(importFinalizedUnclaimedQuestionId); + + vm.prank(forkmanager); + importFinalizedClaimedQuestionId = ForkableRealityETH_ERC20( + forkableRealityETH + ).askQuestion( + UPGRADE_TEMPLATE_ID, + "finalize me then claim", + l1ForkArbitrator, + 1, + 0, + 0 + ); + _setupInitialAnswers(importFinalizedClaimedQuestionId); + + // Push the time forward + vm.warp(block.timestamp + 2); + assert( + ForkableRealityETH_ERC20(forkableRealityETH).isFinalized( + importFinalizedUnclaimedQuestionId + ) + ); + assert( + ForkableRealityETH_ERC20(forkableRealityETH).isFinalized( + importFinalizedClaimedQuestionId + ) + ); + + assertFalse( + ForkableRealityETH_ERC20(forkableRealityETH).isFinalized( + forkOverQuestionId + ) + ); + + vm.prank(forkmanager); + importUnansweredQuestionId = ForkableRealityETH_ERC20( + forkableRealityETH + ).askQuestion( + UPGRADE_TEMPLATE_ID, + "do not answer me", + l1ForkArbitrator, + 60, + 0, + 0 + ); + + vm.prank(forkmanager); + importAnsweredQuestionId = ForkableRealityETH_ERC20(forkableRealityETH) + .askQuestion( + UPGRADE_TEMPLATE_ID, + "answer me but do not finalize", + l1ForkArbitrator, + 60, + 0, + 0 + ); + + _setupInitialAnswers(importAnsweredQuestionId); + } + + // We use the same set of answers for various different questions + function _setupInitialAnswers(bytes32 _questionId) public { + vm.prank(answerGuyYes1); + ForkonomicToken(forkonomicToken).approve( + forkableRealityETH, + uint256(bond1) + ); + vm.prank(answerGuyYes1); + ForkableRealityETH_ERC20(forkableRealityETH).submitAnswerERC20( + _questionId, + answer1, + 0, + bond1 + ); + + vm.prank(answerGuyNo1); + ForkonomicToken(forkonomicToken).approve( + forkableRealityETH, + uint256(bond2) + ); + vm.prank(answerGuyNo1); + ForkableRealityETH_ERC20(forkableRealityETH).submitAnswerERC20( + _questionId, + answer2, + 0, + bond2 + ); + + vm.prank(answerGuyYes2); + ForkonomicToken(forkonomicToken).approve( + forkableRealityETH, + uint256(bond3) + ); + vm.prank(answerGuyYes2); + ForkableRealityETH_ERC20(forkableRealityETH).submitAnswerERC20( + _questionId, + answer3, + 0, + bond3 + ); + } + + // Record what should have been the history hashes for the answers we hard-coded in _setupInitialAnswers + function _setupHistoryHashes() internal { + historyHashes.push( + keccak256( + abi.encodePacked( + bytes32(0), + answer1, + bond1, + answerGuyYes1, + false + ) + ) + ); + historyHashes.push( + keccak256( + abi.encodePacked( + historyHashes[0], + answer2, + bond2, + answerGuyNo1, + false + ) + ) + ); + historyHashes.push( + keccak256( + abi.encodePacked( + historyHashes[1], + answer3, + bond3, + answerGuyYes2, + false + ) + ) + ); + } + + // This does the claim on a finalized question. + // NB doesn't call withdraw, it just leaves the funds in the balance. + // TODO: Also test a partway claim + function _doClaim( + address _forkableRealityETH, + bytes32 _questionId + ) internal { + uint256 ln = historyHashes.length; + bytes32[] memory myHistoryHashes = new bytes32[](ln); + uint256[] memory myBonds = new uint256[](ln); + address[] memory myAnswerers = new address[](ln); + bytes32[] memory myAnswers = new bytes32[](ln); + + myHistoryHashes[0] = historyHashes[1]; + myHistoryHashes[1] = historyHashes[0]; + myHistoryHashes[2] = bytes32(0); + + myBonds[0] = bond3; + myBonds[1] = bond2; + myBonds[2] = bond1; + + myAnswerers[0] = answerGuyYes2; + myAnswerers[1] = answerGuyNo1; + myAnswerers[2] = answerGuyYes1; + + myAnswers[0] = answer3; + myAnswers[1] = answer2; + myAnswers[2] = answer1; + + ForkableRealityETH_ERC20(_forkableRealityETH).claimWinnings( + _questionId, + myHistoryHashes, + myAnswerers, + myBonds, + myAnswers + ); + } + + function _forkTokens( + address _forkonomicToken, + address _forkmanager1, + address _forkmanager2 + ) public returns (address, address) { + address forkonomicToken1; + address forkonomicToken2; + + address parentForkmanager = ForkonomicToken(_forkonomicToken) + .forkmanager(); + + vm.prank(parentForkmanager); + // Fork the token like the forkmanager would do in executeFork + (forkonomicToken1, forkonomicToken2) = IForkonomicToken( + _forkonomicToken + ).createChildren(); + + vm.prank(parentForkmanager); + IForkonomicToken(forkonomicToken1).initialize( + _forkmanager1, + address(_forkonomicToken), + parentForkmanager, // TODO: Is this the right thing to set as minter? + "Child1", //string.concat(IERC20Metadata(address(forkonomicToken)).name(), "0"), + "Child1" // IERC20Metadata(address(_forkonomicToken)).symbol() + ); + vm.prank(forkmanager); + IForkonomicToken(forkonomicToken2).initialize( + _forkmanager2, + address(_forkonomicToken), + parentForkmanager, + "Child2", // string.concat(IERC20Metadata(address(_forkonomicToken)).name(), "1"), + "Child2" // IERC20Metadata(address(_forkonomicToken)).symbol() + ); + + return (forkonomicToken1, forkonomicToken2); + } + + function _forkRealityETH( + address _forkableRealityETH, + address _forkonomicToken1, + address _forkonomicToken2, + bytes32 _forkOverQuestionId, + bool _isAlreadyInitialized + ) internal returns (address, address) { + address parentForkmanager = ForkableRealityETH_ERC20( + _forkableRealityETH + ).forkmanager(); + + if (!_isAlreadyInitialized) { + vm.prank(parentForkmanager); + ForkableRealityETH_ERC20(_forkableRealityETH).handleInitiateFork(); + } + + vm.prank(parentForkmanager); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = ForkableRealityETH_ERC20(_forkableRealityETH).createChildren(); + + vm.prank(parentForkmanager); + ForkableRealityETH_ERC20(forkableRealityETH1).initialize( + ForkonomicToken(_forkonomicToken1).forkmanager(), + _forkableRealityETH, + _forkonomicToken1, + _forkOverQuestionId + ); + vm.prank(parentForkmanager); + ForkableRealityETH_ERC20(forkableRealityETH2).initialize( + ForkonomicToken(_forkonomicToken2).forkmanager(), + _forkableRealityETH, + _forkonomicToken2, + _forkOverQuestionId + ); + + ForkableRealityETH_ERC20(_forkableRealityETH).handleExecuteFork(); + + return (forkableRealityETH1, forkableRealityETH2); + } + + function _testInitialize(address _forkonomicToken) internal { + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH).forkmanager(), + forkmanager + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH).parentContract(), + address(0) + ); + + vm.expectRevert("Initializable: contract is already initialized"); + // vm.expectRevert(IForkableStructure.NotInitializing.selector); In future will be this + ForkableRealityETH_ERC20(forkableRealityETH).initialize( + forkmanager2, + forkableRealityETH, + _forkonomicToken, + bytes32(0) + ); + } + + function testInitialize() public { + _testInitialize(forkonomicToken); + } + + function testClaim() public { + _doClaim(forkableRealityETH, importFinalizedClaimedQuestionId); + } + + function testHandleForkOnlyAfterForking() public { + // Testing revert if children are not yet created + vm.prank(forkmanager); + ForkableRealityETH_ERC20(forkableRealityETH).handleInitiateFork(); + vm.expectRevert(IForkableStructure.OnlyAfterForking.selector); + ForkableRealityETH_ERC20(forkableRealityETH).handleExecuteFork(); + } + + function _testTemplateCreation(address _forkableRealityETH) internal { + assertEq( + ForkableRealityETH_ERC20(_forkableRealityETH).templates( + UPGRADE_TEMPLATE_ID + ), + block.number, + "Template should have been created at the initial block number" + ); + assertEq( + ForkableRealityETH_ERC20(_forkableRealityETH).templates(0), + 0, + "Standard initial template 0 is not created" + ); + assertEq( + ForkableRealityETH_ERC20(_forkableRealityETH).templates(1), + 0, + "Standard initial template 1 is not created" + ); + } + + function testInitialRealityETHTemplateCreation() public { + _testTemplateCreation(forkableRealityETH); + } + + function testRealityETHTemplateCreationOnFork() public { + (address _forkonomicToken1, address _forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + _forkonomicToken1, + _forkonomicToken2, + forkOverQuestionId, + false + ); + _testTemplateCreation(forkableRealityETH1); + _testTemplateCreation(forkableRealityETH2); + } + + function testSplitTokenIntoChildTokens() public { + uint256 initialBalance = ForkonomicToken(forkonomicToken).balanceOf( + forkableRealityETH + ); + assert(initialBalance > 0); + + (address _forkonomicToken1, address _forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address _forkableRealityETH1, + address _forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + _forkonomicToken1, + _forkonomicToken2, + forkOverQuestionId, + false + ); + + // The child reality.eth instances should know which token they belong to + assertEq( + _forkonomicToken1, + address(ForkableRealityETH_ERC20(_forkableRealityETH1).token()), + "child 1 has appropriate token" + ); + assertEq( + _forkonomicToken2, + address(ForkableRealityETH_ERC20(_forkableRealityETH2).token()), + "child 2 has appropriate token" + ); + + assertEq( + ForkonomicToken(forkonomicToken).balanceOf(forkableRealityETH), + 0, + "Parent token balance should be gone" + ); + assertEq( + ForkonomicToken(_forkonomicToken1).balanceOf(forkableRealityETH), + 0, + "Parent reality.eth has nothing in child" + ); + assertEq( + ForkonomicToken(_forkonomicToken1).balanceOf(_forkableRealityETH1), + initialBalance + ); + assertEq( + ForkonomicToken(_forkonomicToken2).balanceOf(_forkableRealityETH2), + initialBalance + ); + } + + function testInitialQuestionImport() public { + (address forkonomicToken1, address forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + forkonomicToken1, + forkonomicToken2, + forkOverQuestionId, + false + ); + + // Both the new reality.eths have the question we forked over, and the original one also still has it + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH).getBestAnswer( + forkOverQuestionId + ), + bytes32(uint256(1)) + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getBestAnswer( + forkOverQuestionId + ), + bytes32(uint256(1)) + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getBestAnswer( + forkOverQuestionId + ), + bytes32(uint256(1)) + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getHistoryHash( + forkOverQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH).getHistoryHash( + forkOverQuestionId + ) + ); + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getHistoryHash( + forkOverQuestionId + ), + bytes32(0) + ); + + // The arbitrator for the imported question will be the arbitrator of the original reality.eth, not the child. + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getArbitrator( + forkOverQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH2).l1ForkArbitrator() + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getArbitrator( + forkOverQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH).l1ForkArbitrator() + ); + } + + function testNoQuestionImport() public { + // If there's no question to import it should simply complete normally without error + (address forkonomicToken1, address forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + _forkRealityETH( + forkableRealityETH, + forkonomicToken1, + forkonomicToken2, + bytes32(0), + false + ); + } + + function testAnsweredQuestionImport() public { + (address forkonomicToken1, address forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + forkonomicToken1, + forkonomicToken2, + forkOverQuestionId, + false + ); + + // Question not imported until we import it + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getHistoryHash( + importAnsweredQuestionId + ), + bytes32(0) + ); + + // Push the time forward to past the time when the original question would normally finalize + vm.warp(block.timestamp + 120); + + ForkableRealityETH_ERC20(forkableRealityETH2).importQuestion( + importAnsweredQuestionId + ); + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getHistoryHash( + importAnsweredQuestionId + ), + bytes32(0) + ); + + // The arbitrator will be set to the child's arbitrator + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getArbitrator( + importAnsweredQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH2).l1ForkArbitrator() + ); + + assertFalse( + ForkableRealityETH_ERC20(forkableRealityETH2).isFinalized( + importAnsweredQuestionId + ) + ); + vm.warp(block.timestamp + 62); + assertTrue( + ForkableRealityETH_ERC20(forkableRealityETH2).isFinalized( + importAnsweredQuestionId + ) + ); + + // Also check the basic import features with the other version + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getHistoryHash( + importAnsweredQuestionId + ), + bytes32(0) + ); + + ForkableRealityETH_ERC20(forkableRealityETH1).importQuestion( + importAnsweredQuestionId + ); + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getHistoryHash( + importAnsweredQuestionId + ), + bytes32(0) + ); + + // The arbitrator will be set to the child's arbitrator + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getArbitrator( + importAnsweredQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH1).l1ForkArbitrator() + ); + } + + function testUnansweredQuestionImport() public { + (address forkonomicToken1, address forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + forkonomicToken1, + forkonomicToken2, + forkOverQuestionId, + false + ); + + // Question not imported until we import it + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getHistoryHash( + importUnansweredQuestionId + ), + bytes32(0) + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getHistoryHash( + importUnansweredQuestionId + ), + bytes32(0) + ); + + // Push the time forward to past the time when the original question would normally finalize if it had been answered + vm.warp(block.timestamp + 120); + assertFalse( + ForkableRealityETH_ERC20(forkableRealityETH2).isFinalized( + importUnansweredQuestionId + ) + ); + + ForkableRealityETH_ERC20(forkableRealityETH2).importQuestion( + importUnansweredQuestionId + ); + + // The arbitrator will be set to the child's arbitrator + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getArbitrator( + importUnansweredQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH2).l1ForkArbitrator() + ); + + // Even after the timeout elapses again, we're still not finalized + vm.warp(block.timestamp + 62); + assertFalse( + ForkableRealityETH_ERC20(forkableRealityETH2).isFinalized( + importUnansweredQuestionId + ) + ); + } + + function testFinalizedUnclaimedQuestionImport() public { + (address forkonomicToken1, address forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + forkonomicToken1, + forkonomicToken2, + forkOverQuestionId, + false + ); + + // Question not imported until we import it + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getHistoryHash( + importFinalizedUnclaimedQuestionId + ), + bytes32(0) + ); + + // Push the time forward to past the time when the original question would normally finalize + vm.warp(block.timestamp + 120); + + ForkableRealityETH_ERC20(forkableRealityETH2).importQuestion( + importFinalizedUnclaimedQuestionId + ); + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getHistoryHash( + importFinalizedUnclaimedQuestionId + ), + bytes32(0) + ); + + // The arbitrator will be set to the child's arbitrator + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getArbitrator( + importFinalizedUnclaimedQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH2).l1ForkArbitrator() + ); + + // Since this was finalized when we did the fork, it's already finalized now. + assertTrue( + ForkableRealityETH_ERC20(forkableRealityETH2).isFinalized( + importFinalizedUnclaimedQuestionId + ) + ); + + // The claim against the parent should fail even though it would have worked before the fork + vm.expectRevert(IRealityETHErrors.ContractIsFrozen.selector); + _doClaim(forkableRealityETH, importFinalizedUnclaimedQuestionId); + + _doClaim(forkableRealityETH2, importFinalizedUnclaimedQuestionId); + + // Also check the basic import features with the other version + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getHistoryHash( + importFinalizedUnclaimedQuestionId + ), + bytes32(0) + ); + + ForkableRealityETH_ERC20(forkableRealityETH1).importQuestion( + importFinalizedUnclaimedQuestionId + ); + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getHistoryHash( + importFinalizedUnclaimedQuestionId + ), + bytes32(0) + ); + + // The arbitrator will be set to the child's arbitrator + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getArbitrator( + importFinalizedUnclaimedQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH1).l1ForkArbitrator() + ); + } + + function testFinalizedClaimedQuestionImport() public { + // We start out already finalized on the parent + assertTrue( + ForkableRealityETH_ERC20(forkableRealityETH).isFinalized( + importFinalizedClaimedQuestionId + ) + ); + + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH).getHistoryHash( + importFinalizedClaimedQuestionId + ), + bytes32(0) + ); + _doClaim(forkableRealityETH, importFinalizedClaimedQuestionId); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH).getHistoryHash( + importFinalizedClaimedQuestionId + ), + bytes32(0) + ); + + (address _forkonomicToken1, address _forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + _forkonomicToken1, + _forkonomicToken2, + forkOverQuestionId, + false + ); + + // Question not imported until we import it + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getTimeout( + importFinalizedClaimedQuestionId + ), + 0 + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getTimeout( + importFinalizedClaimedQuestionId + ), + 0 + ); + + ForkableRealityETH_ERC20(forkableRealityETH2).importQuestion( + importFinalizedClaimedQuestionId + ); + assert( + ForkableRealityETH_ERC20(forkableRealityETH2).getTimeout( + importFinalizedClaimedQuestionId + ) > 0 + ); + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getArbitrator( + importFinalizedClaimedQuestionId + ), + address(0) + ); + + // Since this was finalized when we did the fork, it's already finalized now. + assertTrue( + ForkableRealityETH_ERC20(forkableRealityETH2).isFinalized( + importFinalizedClaimedQuestionId + ) + ); + + // The claim will fail as it's already been done + vm.expectRevert(IRealityETHErrors.ContractIsFrozen.selector); + _doClaim(forkableRealityETH, importFinalizedClaimedQuestionId); + } + + function testMoveBalanceToChildren() public { + // We'll use the claimed question to create an unclaimed balance + _doClaim(forkableRealityETH, importFinalizedClaimedQuestionId); + + // Too early to do this + vm.expectRevert(); + ForkableRealityETH_ERC20(forkableRealityETH).moveBalanceToChildren( + answerGuyYes1 + ); + + (address forkonomicToken1, address forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + forkonomicToken1, + forkonomicToken2, + forkOverQuestionId, + false + ); + + // This moves the internal record that we owe the user money. + // The actual tokens were already transferred in handleExecuteFork() + + // User 1 should have got his bond, then the same again as the takeover fee, minus the claim fee. + uint256 expectedBalanceYes1 = bond1 + bond1 - (bond1 / 40); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH).balanceOf( + answerGuyYes1 + ), + expectedBalanceYes1 + ); + + // Withdraw is banned because we're frozen + vm.prank(answerGuyYes1); + vm.expectRevert(IRealityETHErrors.ContractIsFrozen.selector); + ForkableRealityETH_ERC20(forkableRealityETH).withdraw(); + + // No balance on the child yet + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).balanceOf( + answerGuyYes1 + ), + 0 + ); + + // Anyone can call this for any beneficiary + ForkableRealityETH_ERC20(forkableRealityETH).moveBalanceToChildren( + answerGuyYes1 + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH).balanceOf( + answerGuyYes1 + ), + 0 + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).balanceOf( + answerGuyYes1 + ), + expectedBalanceYes1 + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).balanceOf( + answerGuyYes1 + ), + expectedBalanceYes1 + ); + } + + function testQuestionAskerRestriction() public { + address l1ForkArbitrator = ForkableRealityETH_ERC20(forkableRealityETH) + .l1ForkArbitrator(); + vm.expectRevert(IRealityETHErrors.PermittedQuestionerOnly.selector); + ForkableRealityETH_ERC20(forkableRealityETH).askQuestion( + UPGRADE_TEMPLATE_ID, + "questioner restriction question", + l1ForkArbitrator, + 60, + 0, + 0 + ); + + vm.prank(forkmanager); + ForkableRealityETH_ERC20(forkableRealityETH).askQuestion( + UPGRADE_TEMPLATE_ID, + "questioner restriction question", + l1ForkArbitrator, + 60, + 0, + 0 + ); + } + + function testNextLevelFork() public { + (address forkonomicToken1, address forkonomicToken2) = _forkTokens( + forkonomicToken, + forkmanager1, + forkmanager2 + ); + ( + address forkableRealityETH1, + address forkableRealityETH2 + ) = _forkRealityETH( + forkableRealityETH, + forkonomicToken1, + forkonomicToken2, + forkOverQuestionId, + false + ); + + // Import a question into both forks + ForkableRealityETH_ERC20(forkableRealityETH1).importQuestion( + importFinalizedUnclaimedQuestionId + ); + ForkableRealityETH_ERC20(forkableRealityETH2).importQuestion( + importFinalizedUnclaimedQuestionId + ); + assertNotEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getHistoryHash( + importFinalizedUnclaimedQuestionId + ), + bytes32(0) + ); + + // We'll do the claim on one fork, then fork again on the other fork + _doClaim(forkableRealityETH1, importFinalizedUnclaimedQuestionId); + + (address forkonomicToken2a, address forkonomicToken2b) = _forkTokens( + forkonomicToken2, + forkmanager2a, + forkmanager2b + ); + + assertEq( + IForkonomicToken(forkonomicToken2).parentContract(), + forkonomicToken + ); + assertEq( + IForkonomicToken(forkonomicToken2a).parentContract(), + forkonomicToken2 + ); + assertEq( + IForkonomicToken(forkonomicToken2a).forkmanager(), + forkmanager2a + ); + assertEq( + IForkonomicToken(forkonomicToken2b).forkmanager(), + forkmanager2b + ); + assertEq( + IForkonomicToken(forkonomicToken2).forkmanager(), + ForkableRealityETH_ERC20(forkableRealityETH2).forkmanager() + ); + + ( + address forkableRealityETH2a, + address forkableRealityETH2b + ) = _forkRealityETH( + forkableRealityETH2, + forkonomicToken2a, + forkonomicToken2b, + forkOverQuestionId, + false + ); + + // The arbitrator will be set to the child's arbitrator + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH1).getArbitrator( + importFinalizedUnclaimedQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH1).l1ForkArbitrator() + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2).getArbitrator( + importFinalizedUnclaimedQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH2).l1ForkArbitrator() + ); + + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2a).getArbitrator( + importFinalizedUnclaimedQuestionId + ), + address(0) + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2b).getArbitrator( + importFinalizedUnclaimedQuestionId + ), + address(0) + ); + + ForkableRealityETH_ERC20(forkableRealityETH2a).importQuestion( + importFinalizedUnclaimedQuestionId + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2a).getArbitrator( + importFinalizedUnclaimedQuestionId + ), + ForkableRealityETH_ERC20(forkableRealityETH2a).l1ForkArbitrator() + ); + + // Since this was finalized when we did the fork, it's already finalized now. + assertTrue( + ForkableRealityETH_ERC20(forkableRealityETH).isFinalized( + importFinalizedUnclaimedQuestionId + ) + ); + assertTrue( + ForkableRealityETH_ERC20(forkableRealityETH2).isFinalized( + importFinalizedUnclaimedQuestionId + ) + ); + assertTrue( + ForkableRealityETH_ERC20(forkableRealityETH2a).isFinalized( + importFinalizedUnclaimedQuestionId + ) + ); + } + + function _setupArbitrationRequest( + address _forkableRealityETH, + address _forkRequester, + bytes32 _forkOverQuestionId, + address _forkmanager + ) internal returns (address) { + L1ForkArbitrator l1ForkArbitrator = L1ForkArbitrator( + ForkableRealityETH_ERC20(forkableRealityETH).l1ForkArbitrator() + ); + assertEq(ForkableRealityETH_ERC20(_forkableRealityETH).freezeTs(), 0); + ForkonomicToken token = ForkonomicToken( + address(ForkableRealityETH_ERC20(forkableRealityETH).token()) + ); + uint256 fee = l1ForkArbitrator.getDisputeFee(bytes32(0)); + vm.prank(_forkRequester); + token.approve(address(l1ForkArbitrator), fee); + vm.prank(_forkRequester); + l1ForkArbitrator.requestArbitration(_forkOverQuestionId, 0); + vm.prank(_forkmanager); + // TODO: The add code to the ForkingManager to call this in `initiateFork` which should be called by `requestArbitration` + // Alternatively change the flow so that someone else can do this. See #243 + ForkableRealityETH_ERC20(forkableRealityETH).handleInitiateFork(); + return address(l1ForkArbitrator); + } + + function testRequestArbitration() public { + L1ForkArbitrator l1ForkArbitrator = L1ForkArbitrator( + _setupArbitrationRequest( + forkableRealityETH, + forkRequester, + forkOverQuestionId, + forkmanager + ) + ); + assertEq(l1ForkArbitrator.payer(), forkRequester); + assertEq(l1ForkArbitrator.arbitratingQuestionId(), forkOverQuestionId); + assert(ForkableRealityETH_ERC20(forkableRealityETH).freezeTs() > 0); + assert( + ForkingManager(forkmanager).executionTimeForProposal() > + block.timestamp + ); + } + + function testPostForkHandling() public { + L1ForkArbitrator l1ForkArbitrator = L1ForkArbitrator( + _setupArbitrationRequest( + forkableRealityETH, + forkRequester, + forkOverQuestionId, + forkmanager + ) + ); + vm.warp(ForkingManager(forkmanager).executionTimeForProposal() + 1); + address originalToken = ForkingManager(forkmanager).forkonomicToken(); + uint256 originalBalance = IForkonomicToken(originalToken).balanceOf( + forkableRealityETH + ); + assert(originalBalance > 0); + + ForkingManager(forkmanager).executeFork(); + vm.prank(forkmanager); + (address childForkmanager1, address childForkmanager2) = ForkingManager( + forkmanager + ).getChildren(); + address childToken1 = ForkingManager(childForkmanager1) + .forkonomicToken(); + address childToken2 = ForkingManager(childForkmanager2) + .forkonomicToken(); + + // TODO: Add this logic to executeFork() so we don't need to call it here. + // This will call forkableRealityETH.handleExecuteFork(); + ( + address forkableRealityETH2a, + address forkableRealityETH2b + ) = _forkRealityETH( + forkableRealityETH, + childToken1, + childToken2, + forkOverQuestionId, + true + ); + assertEq( + IForkonomicToken(originalToken).balanceOf(forkableRealityETH), + 0 + ); + assertEq( + IForkonomicToken(childToken1).balanceOf(forkableRealityETH2a), + originalBalance + ); + assertEq( + IForkonomicToken(childToken2).balanceOf(forkableRealityETH2b), + originalBalance + ); + + vm.expectRevert(IRealityETHErrors.QuestionMustBeFinalized.selector); + ForkableRealityETH_ERC20(forkableRealityETH).resultFor( + forkOverQuestionId + ); + vm.expectRevert(IRealityETHErrors.QuestionMustBeFinalized.selector); + ForkableRealityETH_ERC20(forkableRealityETH2a).resultFor( + forkOverQuestionId + ); + vm.expectRevert(IRealityETHErrors.QuestionMustBeFinalized.selector); + ForkableRealityETH_ERC20(forkableRealityETH2b).resultFor( + forkOverQuestionId + ); + + l1ForkArbitrator.settleChildren( + historyHashes[historyHashes.length - 2], + answer3, + answerGuyYes2 + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2a).resultFor( + forkOverQuestionId + ), + bytes32(uint256(1)) + ); + assertEq( + ForkableRealityETH_ERC20(forkableRealityETH2b).resultFor( + forkOverQuestionId + ), + bytes32(uint256(0)) + ); + } +}