@reality.eth/contracts 3.0.48 → 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.
- package/abi/solc-0.8.20/RealityETH-4.0.abi.json +1 -1
- package/abi/solc-0.8.20/RealityETHReopenable-4.0.abi.json +1 -0
- package/abi/solc-0.8.20/RealityETHReopenable_ERC20-4.0.abi.json +1 -0
- package/abi/solc-0.8.20/RealityETH_ERC20-4.0.abi.json +1 -1
- package/bytecode/RealityETH-4.0.bin +1 -1
- package/bytecode/RealityETHReopenable-4.0.bin +1 -0
- package/bytecode/RealityETHReopenable_ERC20-4.0.bin +1 -0
- package/bytecode/RealityETH_ERC20-4.0.bin +1 -1
- package/chains/supported.json +1 -1
- package/development/contracts/IRealityETHCore.sol +0 -15
- package/development/contracts/IRealityETHCore_ERC20.sol +0 -16
- package/development/contracts/IRealityETHHistoryVerification.sol +8 -0
- package/development/contracts/IRealityETHReopenable.sol +22 -0
- package/development/contracts/IRealityETHReopenable_ERC20.sol +23 -0
- package/development/contracts/RealityETH-4.0.sol +50 -105
- package/development/contracts/RealityETHReopenable-4.0.sol +119 -0
- package/development/contracts/RealityETHReopenable_ERC20-4.0.sol +122 -0
- package/development/contracts/RealityETH_ERC20-4.0.sol +50 -108
- package/generated/chains.json +1 -1
- package/package.json +2 -2
- package/tests/python/test.py +81 -5
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
585
|
-
|
|
586
|
-
|
|
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 (
|
|
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
|
+
}
|