@reality.eth/contracts 3.0.47 → 4.0.0-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,10 +3,11 @@
3
3
  pragma solidity ^0.8.20;
4
4
 
5
5
  import {IRealityETHCore} from "./IRealityETHCore.sol";
6
+ import {IRealityETHHistoryVerification} from "./IRealityETHHistoryVerification.sol";
6
7
  import {BalanceHolder} from "./BalanceHolder.sol";
7
8
 
8
9
  // solhint-disable-next-line contract-name-camelcase
9
- contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore {
10
+ contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore, IRealityETHHistoryVerification {
10
11
  address private constant NULL_ADDRESS = address(0);
11
12
 
12
13
  // History hash when no history is created, or history has been cleared
@@ -18,18 +19,12 @@ contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore {
18
19
  // Proportion withheld when you claim an earlier bond.
19
20
  uint256 private constant BOND_CLAIM_FEE_PROPORTION = 40; // One 40th ie 2.5%
20
21
 
21
- // Special value representing a question that was answered too soon.
22
- // bytes32(-2). By convention we use bytes32(-1) for "invalid", although the contract does not handle this.
23
- bytes32 private constant UNRESOLVED_ANSWER = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe;
24
-
25
22
  uint256 private nextTemplateID = 0;
26
23
  mapping(uint256 => uint256) public templates;
27
24
  mapping(uint256 => bytes32) public template_hashes;
28
25
  mapping(bytes32 => Question) public questions;
29
26
  mapping(bytes32 => Claim) public question_claims;
30
27
  mapping(address => uint256) public arbitrator_question_fees;
31
- mapping(bytes32 => bytes32) public reopened_questions;
32
- mapping(bytes32 => bool) public reopener_questions;
33
28
 
34
29
  modifier onlyArbitrator(bytes32 question_id) {
35
30
  if (msg.sender != questions[question_id].arbitrator) revert MsgSenderMustBeArbitrator();
@@ -376,7 +371,9 @@ contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore {
376
371
  /// @param last_answer The last answer given
377
372
  /// @param last_answerer The address that supplied the last answer
378
373
  function assignWinnerAndSubmitAnswerByArbitrator(bytes32 question_id, bytes32 answer, address payee_if_wrong, bytes32 last_history_hash, bytes32 last_answer, address last_answerer) external {
379
- _verifyHistoryInputOrRevert(questions[question_id].history_hash, last_history_hash, last_answer, questions[question_id].bond, last_answerer);
374
+ if (!_isHistoryInputValidForHash(questions[question_id].history_hash, last_history_hash, last_answer, questions[question_id].bond, last_answerer)) {
375
+ revert HistoryInputProvidedDidNotMatchTheExpectedHash();
376
+ }
380
377
 
381
378
  address payee = (questions[question_id].best_answer == answer) ? last_answerer : payee_if_wrong;
382
379
  submitAnswerByArbitrator(question_id, answer, payee);
@@ -404,97 +401,6 @@ contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore {
404
401
  return questions[question_id].best_answer;
405
402
  }
406
403
 
407
- /// @notice Returns whether the question was answered before it had an answer, ie resolved to UNRESOLVED_ANSWER
408
- /// @param question_id The ID of the question
409
- function isSettledTooSoon(bytes32 question_id) public view returns (bool) {
410
- return (resultFor(question_id) == UNRESOLVED_ANSWER);
411
- }
412
-
413
- /// @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
414
- /// @param question_id The ID of the question
415
- function resultForOnceSettled(bytes32 question_id) external view returns (bytes32) {
416
- bytes32 result = resultFor(question_id);
417
- if (result == UNRESOLVED_ANSWER) {
418
- // Try the replacement
419
- bytes32 replacement_id = reopened_questions[question_id];
420
- if (replacement_id == bytes32(0x0)) revert QuestionWasSettledTooSoonAndHasNotBeenReopened();
421
- // We only try one layer down rather than recursing to keep the gas costs predictable
422
- result = resultFor(replacement_id);
423
- if (result == UNRESOLVED_ANSWER) revert QuestionReplacementWasSettledTooSoonAndHasNotBeenReopened();
424
- }
425
- return result;
426
- }
427
-
428
- /// @notice Asks a new question reopening a previously-asked question that was settled too soon
429
- /// @dev A special version of askQuestion() that replaces a previous question that was settled too soon
430
- /// @param template_id The ID number of the template the question will use
431
- /// @param question A string containing the parameters that will be passed into the template to make the question
432
- /// @param arbitrator The arbitration contract that will have the final word on the answer if there is a dispute
433
- /// @param timeout How long the contract should wait after the answer is changed before finalizing on that answer
434
- /// @param opening_ts If set, the earliest time it should be possible to answer the question.
435
- /// @param nonce A user-specified nonce used in the question ID. Change it to repeat a question.
436
- /// @param min_bond The minimum bond that can be used to provide the first answer.
437
- /// @param reopens_question_id The ID of the question this reopens
438
- /// @return The ID of the newly-created question, created deterministically.
439
- function reopenQuestion(
440
- uint256 template_id,
441
- string memory question,
442
- address arbitrator,
443
- uint32 timeout,
444
- uint32 opening_ts,
445
- uint256 nonce,
446
- uint256 min_bond,
447
- bytes32 reopens_question_id
448
- )
449
- public
450
- payable
451
- returns (
452
- // stateNotCreated is enforced by the internal _askQuestion
453
- bytes32
454
- )
455
- {
456
- if (!isSettledTooSoon(reopens_question_id)) revert YouCanOnlyReopenQuestionsThatResolvedAsSettledTooSoon();
457
-
458
- bytes32 content_hash = keccak256(abi.encodePacked(template_id, opening_ts, question));
459
-
460
- // A reopening must exactly match the original question, except for the nonce and the creator
461
- if (content_hash != questions[reopens_question_id].content_hash) revert ContentHashMismatch();
462
- if (arbitrator != questions[reopens_question_id].arbitrator) revert ArbitratorMismatch();
463
- if (timeout != questions[reopens_question_id].timeout) revert TimeoutMismatch();
464
- if (opening_ts != questions[reopens_question_id].opening_ts) revert Opening_TsMismatch();
465
- if (min_bond != questions[reopens_question_id].min_bond) revert Min_BondMismatch();
466
-
467
- // If the the question was itself reopening some previous question, you'll have to re-reopen the previous question first.
468
- // This ensures the bounty can be passed on to the next attempt of the original question.
469
- if (reopener_questions[reopens_question_id]) revert QuestionIsAlreadyReopeningAPreviousQuestion();
470
-
471
- // A question can only be reopened once, unless the reopening was also settled too soon in which case it can be replaced
472
- bytes32 existing_reopen_question_id = reopened_questions[reopens_question_id];
473
-
474
- // Normally when we reopen a question we will take its bounty and pass it on to the reopened version.
475
- bytes32 take_bounty_from_question_id = reopens_question_id;
476
- // If the question has already been reopened but was again settled too soon, we can transfer its bounty to the next attempt.
477
- if (existing_reopen_question_id != bytes32(0)) {
478
- if (!isSettledTooSoon(existing_reopen_question_id)) revert QuestionHasAlreadyBeenReopened();
479
- // We'll overwrite the reopening with our new question and move the bounty.
480
- // 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.
481
- reopener_questions[existing_reopen_question_id] = false;
482
- take_bounty_from_question_id = existing_reopen_question_id;
483
- }
484
-
485
- bytes32 question_id = askQuestionWithMinBond(template_id, question, arbitrator, timeout, opening_ts, nonce, min_bond);
486
-
487
- reopened_questions[reopens_question_id] = question_id;
488
- reopener_questions[question_id] = true;
489
-
490
- questions[question_id].bounty = questions[take_bounty_from_question_id].bounty + questions[question_id].bounty;
491
- questions[take_bounty_from_question_id].bounty = 0;
492
-
493
- emit LogReopenQuestion(question_id, reopens_question_id);
494
-
495
- return question_id;
496
- }
497
-
498
404
  /// @notice Return the final answer to the specified question, provided it matches the specified criteria.
499
405
  /// @dev Reverts if the question is not finalized, or if it does not match the specified criteria.
500
406
  /// @param question_id The ID of the question
@@ -541,7 +447,9 @@ contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore {
541
447
 
542
448
  uint256 i;
543
449
  for (i = 0; i < history_hashes.length; i++) {
544
- _verifyHistoryInputOrRevert(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i]);
450
+ if (!_isHistoryInputValidForHash(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i])) {
451
+ revert HistoryInputProvidedDidNotMatchTheExpectedHash();
452
+ }
545
453
 
546
454
  queued_funds = queued_funds + last_bond;
547
455
  (queued_funds, payee) = _processHistoryItem(question_id, best_answer, queued_funds, payee, addrs[i], bonds[i], answers[i]);
@@ -581,10 +489,13 @@ contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore {
581
489
  emit LogClaim(question_id, payee, value);
582
490
  }
583
491
 
584
- function _verifyHistoryInputOrRevert(bytes32 last_history_hash, bytes32 history_hash, bytes32 answer, uint256 bond, address addr) internal pure {
585
- if (last_history_hash != keccak256(abi.encodePacked(history_hash, answer, bond, addr, false))) {
586
- revert HistoryInputProvidedDidNotMatchTheExpectedHash();
587
- }
492
+ function _isHistoryInputValidForHash(bytes32 last_history_hash, bytes32 history_hash, bytes32 answer, uint256 bond, address addr) internal pure returns (bool) {
493
+ return (last_history_hash == keccak256(abi.encodePacked(history_hash, answer, bond, addr, false)));
494
+ }
495
+
496
+ // The answered-too-soon version will override this with (answer != UNRESOLVED_ANSWER)
497
+ function _isBountyPayableOnAnswer(bytes32) internal pure virtual returns (bool) {
498
+ return true;
588
499
  }
589
500
 
590
501
  function _processHistoryItem(bytes32 question_id, bytes32 best_answer, uint256 queued_funds, address payee, address addr, uint256 bond, bytes32 answer) internal returns (uint256, address) {
@@ -594,7 +505,7 @@ contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore {
594
505
  // They get the question bounty.
595
506
  payee = addr;
596
507
 
597
- if (best_answer != UNRESOLVED_ANSWER && questions[question_id].bounty > 0) {
508
+ if (questions[question_id].bounty > 0 && _isBountyPayableOnAnswer(best_answer)) {
598
509
  _payPayee(question_id, payee, questions[question_id].bounty);
599
510
  questions[question_id].bounty = 0;
600
511
  }
@@ -662,6 +573,40 @@ contract RealityETH_v4_0 is BalanceHolder, IRealityETHCore {
662
573
  withdraw();
663
574
  }
664
575
 
576
+ /// @notice Returns true if the supplied history is valid
577
+ /// @dev Caller must provide the answer history, in reverse order back to the item they want to check
578
+ /// @dev Not necessarily the entire history
579
+ /// @dev Useful for freezing an action once a bond is paid for a particular answer, without waiting for resolution
580
+ /// @dev Cannot be used after the question is finalized
581
+ /// @param question_id The ID of the question
582
+ /// @param history_hashes Second-last-to-first, the hash of each history entry. (Final one should be empty).
583
+ /// @param addrs Last-to-first, the address of each answerer
584
+ /// @param bonds Last-to-first, the bond supplied with each answer
585
+ /// @param answers Last-to-first, each answer supplied
586
+ function isHistoryOfUnfinalizedQuestionValid(
587
+ bytes32 question_id,
588
+ bytes32[] memory history_hashes,
589
+ address[] memory addrs,
590
+ uint256[] memory bonds,
591
+ bytes32[] memory answers
592
+ ) external view stateOpenOrPendingArbitration(question_id) returns (bool) {
593
+ bytes32 last_history_hash = questions[question_id].history_hash;
594
+
595
+ uint256 hist_len = history_hashes.length;
596
+ // Check for uneven length entries to make sure we validate all the inputs
597
+ if (addrs.length != hist_len || bonds.length != hist_len || answers.length != hist_len) {
598
+ return false;
599
+ }
600
+
601
+ for (uint256 i = 0; i < hist_len; i++) {
602
+ if (!_isHistoryInputValidForHash(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i])) {
603
+ return false;
604
+ }
605
+ last_history_hash = history_hashes[i];
606
+ }
607
+ return true;
608
+ }
609
+
665
610
  /// @notice Returns the questions's content hash, identifying the question content
666
611
  /// @param question_id The ID of the question
667
612
  function getContentHash(bytes32 question_id) public view returns (bytes32) {
@@ -0,0 +1,119 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+
3
+ pragma solidity ^0.8.20;
4
+
5
+ import {IRealityETHReopenable} from "./IRealityETHReopenable.sol";
6
+ import {RealityETH_v4_0} from "./RealityETH-4.0.sol";
7
+
8
+ /*
9
+ * This version of reality.eth provides the ability to answer a question as "answered too soon", allowing it to be reopened.
10
+ * This is useful when you want to ask a question but you don't know when the answer will be available.
11
+ * You can set the settlement time at the earliest date it may be available, then if someone answers it too soon, it can be answered at such then later reopened.
12
+ * NB In v3 of reality.eth this feature was built in and there was no way to turn it off.
13
+ * From v4, we ship two versions: RealityETH without it, and RealityETHReopenable with it.
14
+ */
15
+
16
+ // solhint-disable-next-line contract-name-camelcase
17
+ contract RealityETHReopenable_v4_0 is IRealityETHReopenable, RealityETH_v4_0 {
18
+ // Special value representing a question that was answered too soon.
19
+ // bytes32(-2). By convention we use bytes32(-1) for "invalid", although the contract does not handle this.
20
+ bytes32 private constant UNRESOLVED_ANSWER = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe;
21
+
22
+ mapping(bytes32 => bytes32) public reopened_questions;
23
+ mapping(bytes32 => bool) public reopener_questions;
24
+
25
+ function _isBountyPayableOnAnswer(bytes32 answer) internal pure override returns (bool) {
26
+ return (answer != UNRESOLVED_ANSWER);
27
+ }
28
+
29
+ /// @notice Returns whether the question was answered before it had an answer, ie resolved to UNRESOLVED_ANSWER
30
+ /// @param question_id The ID of the question
31
+ function isSettledTooSoon(bytes32 question_id) public view returns (bool) {
32
+ return (resultFor(question_id) == UNRESOLVED_ANSWER);
33
+ }
34
+
35
+ /// @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
36
+ /// @param question_id The ID of the question
37
+ function resultForOnceSettled(bytes32 question_id) external view returns (bytes32) {
38
+ bytes32 result = resultFor(question_id);
39
+ if (result == UNRESOLVED_ANSWER) {
40
+ // Try the replacement
41
+ bytes32 replacement_id = reopened_questions[question_id];
42
+ if (replacement_id == bytes32(0x0)) revert QuestionWasSettledTooSoonAndHasNotBeenReopened();
43
+ // We only try one layer down rather than recursing to keep the gas costs predictable
44
+ result = resultFor(replacement_id);
45
+ if (result == UNRESOLVED_ANSWER) revert QuestionReplacementWasSettledTooSoonAndHasNotBeenReopened();
46
+ }
47
+ return result;
48
+ }
49
+
50
+ /// @notice Asks a new question reopening a previously-asked question that was settled too soon
51
+ /// @dev A special version of askQuestion() that replaces a previous question that was settled too soon
52
+ /// @param template_id The ID number of the template the question will use
53
+ /// @param question A string containing the parameters that will be passed into the template to make the question
54
+ /// @param arbitrator The arbitration contract that will have the final word on the answer if there is a dispute
55
+ /// @param timeout How long the contract should wait after the answer is changed before finalizing on that answer
56
+ /// @param opening_ts If set, the earliest time it should be possible to answer the question.
57
+ /// @param nonce A user-specified nonce used in the question ID. Change it to repeat a question.
58
+ /// @param min_bond The minimum bond that can be used to provide the first answer.
59
+ /// @param reopens_question_id The ID of the question this reopens
60
+ /// @return The ID of the newly-created question, created deterministically.
61
+ function reopenQuestion(
62
+ uint256 template_id,
63
+ string memory question,
64
+ address arbitrator,
65
+ uint32 timeout,
66
+ uint32 opening_ts,
67
+ uint256 nonce,
68
+ uint256 min_bond,
69
+ bytes32 reopens_question_id
70
+ )
71
+ public
72
+ payable
73
+ returns (
74
+ // stateNotCreated is enforced by the internal _askQuestion
75
+ bytes32
76
+ )
77
+ {
78
+ if (!isSettledTooSoon(reopens_question_id)) revert YouCanOnlyReopenQuestionsThatResolvedAsSettledTooSoon();
79
+
80
+ bytes32 content_hash = keccak256(abi.encodePacked(template_id, opening_ts, question));
81
+
82
+ // A reopening must exactly match the original question, except for the nonce and the creator
83
+ if (content_hash != questions[reopens_question_id].content_hash) revert ContentHashMismatch();
84
+ if (arbitrator != questions[reopens_question_id].arbitrator) revert ArbitratorMismatch();
85
+ if (timeout != questions[reopens_question_id].timeout) revert TimeoutMismatch();
86
+ if (opening_ts != questions[reopens_question_id].opening_ts) revert Opening_TsMismatch();
87
+ if (min_bond != questions[reopens_question_id].min_bond) revert Min_BondMismatch();
88
+
89
+ // If the the question was itself reopening some previous question, you'll have to re-reopen the previous question first.
90
+ // This ensures the bounty can be passed on to the next attempt of the original question.
91
+ if (reopener_questions[reopens_question_id]) revert QuestionIsAlreadyReopeningAPreviousQuestion();
92
+
93
+ // A question can only be reopened once, unless the reopening was also settled too soon in which case it can be replaced
94
+ bytes32 existing_reopen_question_id = reopened_questions[reopens_question_id];
95
+
96
+ // Normally when we reopen a question we will take its bounty and pass it on to the reopened version.
97
+ bytes32 take_bounty_from_question_id = reopens_question_id;
98
+ // If the question has already been reopened but was again settled too soon, we can transfer its bounty to the next attempt.
99
+ if (existing_reopen_question_id != bytes32(0)) {
100
+ if (!isSettledTooSoon(existing_reopen_question_id)) revert QuestionHasAlreadyBeenReopened();
101
+ // We'll overwrite the reopening with our new question and move the bounty.
102
+ // 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.
103
+ reopener_questions[existing_reopen_question_id] = false;
104
+ take_bounty_from_question_id = existing_reopen_question_id;
105
+ }
106
+
107
+ bytes32 question_id = askQuestionWithMinBond(template_id, question, arbitrator, timeout, opening_ts, nonce, min_bond);
108
+
109
+ reopened_questions[reopens_question_id] = question_id;
110
+ reopener_questions[question_id] = true;
111
+
112
+ questions[question_id].bounty = questions[take_bounty_from_question_id].bounty + questions[question_id].bounty;
113
+ questions[take_bounty_from_question_id].bounty = 0;
114
+
115
+ emit LogReopenQuestion(question_id, reopens_question_id);
116
+
117
+ return question_id;
118
+ }
119
+ }
@@ -0,0 +1,122 @@
1
+ // SPDX-License-Identifier: GPL-3.0-only
2
+
3
+ pragma solidity ^0.8.20;
4
+
5
+ import {IRealityETHReopenable_ERC20} from "./IRealityETHReopenable_ERC20.sol";
6
+ import {RealityETH_ERC20_v4_0} from "./RealityETH_ERC20-4.0.sol";
7
+
8
+ /*
9
+ * This version of reality.eth provides the ability to answer a question as "answered too soon", allowing it to be reopened.
10
+ * This is useful when you want to ask a question but you don't know when the answer will be available.
11
+ * You can set the settlement time at the earliest date it may be available, then if someone answers it too soon, it can be answered at such then later reopened.
12
+ * NB In v3 of reality.eth this feature was built in and there was no way to turn it off.
13
+ * From v4, we ship two versions: RealityETH_ERC20 without it, and RealityETHReopenable_ERC20 with it.
14
+ */
15
+
16
+ // solhint-disable-next-line contract-name-camelcase
17
+ contract RealityETHReopenable_v4_0 is IRealityETHReopenable_ERC20, RealityETH_ERC20_v4_0 {
18
+ // Special value representing a question that was answered too soon.
19
+ // bytes32(-2). By convention we use bytes32(-1) for "invalid", although the contract does not handle this.
20
+ bytes32 private constant UNRESOLVED_ANSWER = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe;
21
+
22
+ mapping(bytes32 => bytes32) public reopened_questions;
23
+ mapping(bytes32 => bool) public reopener_questions;
24
+
25
+ function _isBountyPayableOnAnswer(bytes32 answer) internal pure override returns (bool) {
26
+ return (answer != UNRESOLVED_ANSWER);
27
+ }
28
+
29
+ /// @notice Returns whether the question was answered before it had an answer, ie resolved to UNRESOLVED_ANSWER
30
+ /// @param question_id The ID of the question
31
+ function isSettledTooSoon(bytes32 question_id) public view returns (bool) {
32
+ return (resultFor(question_id) == UNRESOLVED_ANSWER);
33
+ }
34
+
35
+ /// @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
36
+ /// @param question_id The ID of the question
37
+ function resultForOnceSettled(bytes32 question_id) external view returns (bytes32) {
38
+ bytes32 result = resultFor(question_id);
39
+ if (result == UNRESOLVED_ANSWER) {
40
+ // Try the replacement
41
+ bytes32 replacement_id = reopened_questions[question_id];
42
+ if (replacement_id == bytes32(0x0)) revert QuestionWasSettledTooSoonAndHasNotBeenReopened();
43
+ // We only try one layer down rather than recursing to keep the gas costs predictable
44
+ result = resultFor(replacement_id);
45
+ if (result == UNRESOLVED_ANSWER) revert QuestionReplacementWasSettledTooSoonAndHasNotBeenReopened();
46
+ }
47
+ return result;
48
+ }
49
+
50
+ /// @notice Asks a new question reopening a previously-asked question that was settled too soon
51
+ /// @dev A special version of askQuestion() that replaces a previous question that was settled too soon
52
+ /// @param template_id The ID number of the template the question will use
53
+ /// @param question A string containing the parameters that will be passed into the template to make the question
54
+ /// @param arbitrator The arbitration contract that will have the final word on the answer if there is a dispute
55
+ /// @param timeout How long the contract should wait after the answer is changed before finalizing on that answer
56
+ /// @param opening_ts If set, the earliest time it should be possible to answer the question.
57
+ /// @param nonce A user-specified nonce used in the question ID. Change it to repeat a question.
58
+ /// @param min_bond The minimum bond that can be used to provide the first answer.
59
+ /// @param reopens_question_id The ID of the question this reopens
60
+ /// @param tokens The number of tokens you want to use as an additional question reward for the reopened question.
61
+ /// @return The ID of the newly-created question, created deterministically.
62
+ function reopenQuestionERC20(
63
+ uint256 template_id,
64
+ string memory question,
65
+ address arbitrator,
66
+ uint32 timeout,
67
+ uint32 opening_ts,
68
+ uint256 nonce,
69
+ uint256 min_bond,
70
+ bytes32 reopens_question_id,
71
+ uint256 tokens
72
+ )
73
+ public
74
+ returns (
75
+ // stateNotCreated is enforced by the internal _askQuestion
76
+ bytes32
77
+ )
78
+ {
79
+ // _deductTokensOrRevert will be called when we call askQuestionWithMinBondERC20
80
+
81
+ if (!isSettledTooSoon(reopens_question_id)) revert YouCanOnlyReopenQuestionsThatResolvedAsSettledTooSoon();
82
+
83
+ bytes32 content_hash = keccak256(abi.encodePacked(template_id, opening_ts, question));
84
+
85
+ // A reopening must exactly match the original question, except for the nonce and the creator
86
+ if (content_hash != questions[reopens_question_id].content_hash) revert ContentHashMismatch();
87
+ if (arbitrator != questions[reopens_question_id].arbitrator) revert ArbitratorMismatch();
88
+ if (timeout != questions[reopens_question_id].timeout) revert TimeoutMismatch();
89
+ if (opening_ts != questions[reopens_question_id].opening_ts) revert Opening_TsMismatch();
90
+ if (min_bond != questions[reopens_question_id].min_bond) revert Min_BondMismatch();
91
+
92
+ // If the the question was itself reopening some previous question, you'll have to re-reopen the previous question first.
93
+ // This ensures the bounty can be passed on to the next attempt of the original question.
94
+ if (reopener_questions[reopens_question_id]) revert QuestionIsAlreadyReopeningAPreviousQuestion();
95
+
96
+ // A question can only be reopened once, unless the reopening was also settled too soon in which case it can be replaced
97
+ bytes32 existing_reopen_question_id = reopened_questions[reopens_question_id];
98
+
99
+ // Normally when we reopen a question we will take its bounty and pass it on to the reopened version.
100
+ bytes32 take_bounty_from_question_id = reopens_question_id;
101
+ // If the question has already been reopened but was again settled too soon, we can transfer its bounty to the next attempt.
102
+ if (existing_reopen_question_id != bytes32(0)) {
103
+ if (!isSettledTooSoon(existing_reopen_question_id)) revert QuestionHasAlreadyBeenReopened();
104
+ // We'll overwrite the reopening with our new question and move the bounty.
105
+ // 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.
106
+ reopener_questions[existing_reopen_question_id] = false;
107
+ take_bounty_from_question_id = existing_reopen_question_id;
108
+ }
109
+
110
+ bytes32 question_id = askQuestionWithMinBondERC20(template_id, question, arbitrator, timeout, opening_ts, nonce, min_bond, tokens);
111
+
112
+ reopened_questions[reopens_question_id] = question_id;
113
+ reopener_questions[question_id] = true;
114
+
115
+ questions[question_id].bounty = questions[take_bounty_from_question_id].bounty + questions[question_id].bounty;
116
+ questions[take_bounty_from_question_id].bounty = 0;
117
+
118
+ emit LogReopenQuestion(question_id, reopens_question_id);
119
+
120
+ return question_id;
121
+ }
122
+ }