@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.
@@ -4,10 +4,11 @@ pragma solidity ^0.8.20;
4
4
 
5
5
  import {IERC20} from "./IERC20.sol";
6
6
  import {IRealityETHCore_ERC20} from "./IRealityETHCore_ERC20.sol";
7
+ import {IRealityETHHistoryVerification} from "./IRealityETHHistoryVerification.sol";
7
8
  import {BalanceHolder_ERC20} from "./BalanceHolder_ERC20.sol";
8
9
 
9
10
  // solhint-disable-next-line contract-name-camelcase
10
- contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20 {
11
+ contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20, IRealityETHHistoryVerification {
11
12
  address private constant NULL_ADDRESS = address(0);
12
13
 
13
14
  // History hash when no history is created, or history has been cleared
@@ -19,18 +20,12 @@ contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20 {
19
20
  // Proportion withheld when you claim an earlier bond.
20
21
  uint256 private constant BOND_CLAIM_FEE_PROPORTION = 40; // One 40th ie 2.5%
21
22
 
22
- // Special value representing a question that was answered too soon.
23
- // bytes32(-2). By convention we use bytes32(-1) for "invalid", although the contract does not handle this.
24
- bytes32 private constant UNRESOLVED_ANSWER = 0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe;
25
-
26
23
  uint256 private nextTemplateID = 0;
27
24
  mapping(uint256 => uint256) public templates;
28
25
  mapping(uint256 => bytes32) public template_hashes;
29
26
  mapping(bytes32 => Question) public questions;
30
27
  mapping(bytes32 => Claim) public question_claims;
31
28
  mapping(address => uint256) public arbitrator_question_fees;
32
- mapping(bytes32 => bytes32) public reopened_questions;
33
- mapping(bytes32 => bool) public reopener_questions;
34
29
 
35
30
  modifier onlyArbitrator(bytes32 question_id) {
36
31
  if (msg.sender != questions[question_id].arbitrator) revert MsgSenderMustBeArbitrator();
@@ -457,7 +452,9 @@ contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20 {
457
452
  /// @param last_answer The last answer given
458
453
  /// @param last_answerer The address that supplied the last answer
459
454
  function assignWinnerAndSubmitAnswerByArbitrator(bytes32 question_id, bytes32 answer, address payee_if_wrong, bytes32 last_history_hash, bytes32 last_answer, address last_answerer) external {
460
- _verifyHistoryInputOrRevert(questions[question_id].history_hash, last_history_hash, last_answer, questions[question_id].bond, last_answerer);
455
+ if (!_isHistoryInputValidForHash(questions[question_id].history_hash, last_history_hash, last_answer, questions[question_id].bond, last_answerer)) {
456
+ revert HistoryInputProvidedDidNotMatchTheExpectedHash();
457
+ }
461
458
 
462
459
  address payee = (questions[question_id].best_answer == answer) ? last_answerer : payee_if_wrong;
463
460
  submitAnswerByArbitrator(question_id, answer, payee);
@@ -485,100 +482,6 @@ contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20 {
485
482
  return questions[question_id].best_answer;
486
483
  }
487
484
 
488
- /// @notice Returns whether the question was answered before it had an answer, ie resolved to UNRESOLVED_ANSWER
489
- /// @param question_id The ID of the question
490
- function isSettledTooSoon(bytes32 question_id) public view returns (bool) {
491
- return (resultFor(question_id) == UNRESOLVED_ANSWER);
492
- }
493
-
494
- /// @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
495
- /// @param question_id The ID of the question
496
- function resultForOnceSettled(bytes32 question_id) external view returns (bytes32) {
497
- bytes32 result = resultFor(question_id);
498
- if (result == UNRESOLVED_ANSWER) {
499
- // Try the replacement
500
- bytes32 replacement_id = reopened_questions[question_id];
501
- if (replacement_id == bytes32(0x0)) revert QuestionWasSettledTooSoonAndHasNotBeenReopened();
502
- // We only try one layer down rather than recursing to keep the gas costs predictable
503
- result = resultFor(replacement_id);
504
- if (result == UNRESOLVED_ANSWER) revert QuestionReplacementWasSettledTooSoonAndHasNotBeenReopened();
505
- }
506
- return result;
507
- }
508
-
509
- /// @notice Asks a new question reopening a previously-asked question that was settled too soon
510
- /// @dev A special version of askQuestion() that replaces a previous question that was settled too soon
511
- /// @param template_id The ID number of the template the question will use
512
- /// @param question A string containing the parameters that will be passed into the template to make the question
513
- /// @param arbitrator The arbitration contract that will have the final word on the answer if there is a dispute
514
- /// @param timeout How long the contract should wait after the answer is changed before finalizing on that answer
515
- /// @param opening_ts If set, the earliest time it should be possible to answer the question.
516
- /// @param nonce A user-specified nonce used in the question ID. Change it to repeat a question.
517
- /// @param min_bond The minimum bond that can be used to provide the first answer.
518
- /// @param reopens_question_id The ID of the question this reopens
519
- /// @param tokens The number of tokens you want to use as an additional question reward for the reopened question.
520
- /// @return The ID of the newly-created question, created deterministically.
521
- function reopenQuestionERC20(
522
- uint256 template_id,
523
- string memory question,
524
- address arbitrator,
525
- uint32 timeout,
526
- uint32 opening_ts,
527
- uint256 nonce,
528
- uint256 min_bond,
529
- bytes32 reopens_question_id,
530
- uint256 tokens
531
- )
532
- public
533
- returns (
534
- // stateNotCreated is enforced by the internal _askQuestion
535
- bytes32
536
- )
537
- {
538
- // _deductTokensOrRevert will be called when we call askQuestionWithMinBondERC20
539
-
540
- if (!isSettledTooSoon(reopens_question_id)) revert YouCanOnlyReopenQuestionsThatResolvedAsSettledTooSoon();
541
-
542
- bytes32 content_hash = keccak256(abi.encodePacked(template_id, opening_ts, question));
543
-
544
- // A reopening must exactly match the original question, except for the nonce and the creator
545
- if (content_hash != questions[reopens_question_id].content_hash) revert ContentHashMismatch();
546
- if (arbitrator != questions[reopens_question_id].arbitrator) revert ArbitratorMismatch();
547
- if (timeout != questions[reopens_question_id].timeout) revert TimeoutMismatch();
548
- if (opening_ts != questions[reopens_question_id].opening_ts) revert Opening_TsMismatch();
549
- if (min_bond != questions[reopens_question_id].min_bond) revert Min_BondMismatch();
550
-
551
- // If the the question was itself reopening some previous question, you'll have to re-reopen the previous question first.
552
- // This ensures the bounty can be passed on to the next attempt of the original question.
553
- if (reopener_questions[reopens_question_id]) revert QuestionIsAlreadyReopeningAPreviousQuestion();
554
-
555
- // A question can only be reopened once, unless the reopening was also settled too soon in which case it can be replaced
556
- bytes32 existing_reopen_question_id = reopened_questions[reopens_question_id];
557
-
558
- // Normally when we reopen a question we will take its bounty and pass it on to the reopened version.
559
- bytes32 take_bounty_from_question_id = reopens_question_id;
560
- // If the question has already been reopened but was again settled too soon, we can transfer its bounty to the next attempt.
561
- if (existing_reopen_question_id != bytes32(0)) {
562
- if (!isSettledTooSoon(existing_reopen_question_id)) revert QuestionHasAlreadyBeenReopened();
563
- // We'll overwrite the reopening with our new question and move the bounty.
564
- // 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.
565
- reopener_questions[existing_reopen_question_id] = false;
566
- take_bounty_from_question_id = existing_reopen_question_id;
567
- }
568
-
569
- bytes32 question_id = askQuestionWithMinBondERC20(template_id, question, arbitrator, timeout, opening_ts, nonce, min_bond, tokens);
570
-
571
- reopened_questions[reopens_question_id] = question_id;
572
- reopener_questions[question_id] = true;
573
-
574
- questions[question_id].bounty = questions[take_bounty_from_question_id].bounty + questions[question_id].bounty;
575
- questions[take_bounty_from_question_id].bounty = 0;
576
-
577
- emit LogReopenQuestion(question_id, reopens_question_id);
578
-
579
- return question_id;
580
- }
581
-
582
485
  /// @notice Return the final answer to the specified question, provided it matches the specified criteria.
583
486
  /// @dev Reverts if the question is not finalized, or if it does not match the specified criteria.
584
487
  /// @param question_id The ID of the question
@@ -625,7 +528,9 @@ contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20 {
625
528
 
626
529
  uint256 i;
627
530
  for (i = 0; i < history_hashes.length; i++) {
628
- _verifyHistoryInputOrRevert(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i]);
531
+ if (!_isHistoryInputValidForHash(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i])) {
532
+ revert HistoryInputProvidedDidNotMatchTheExpectedHash();
533
+ }
629
534
 
630
535
  queued_funds = queued_funds + last_bond;
631
536
  (queued_funds, payee) = _processHistoryItem(question_id, best_answer, queued_funds, payee, addrs[i], bonds[i], answers[i]);
@@ -665,10 +570,13 @@ contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20 {
665
570
  emit LogClaim(question_id, payee, value);
666
571
  }
667
572
 
668
- function _verifyHistoryInputOrRevert(bytes32 last_history_hash, bytes32 history_hash, bytes32 answer, uint256 bond, address addr) internal pure {
669
- if (last_history_hash != keccak256(abi.encodePacked(history_hash, answer, bond, addr, false))) {
670
- revert HistoryInputProvidedDidNotMatchTheExpectedHash();
671
- }
573
+ function _isHistoryInputValidForHash(bytes32 last_history_hash, bytes32 history_hash, bytes32 answer, uint256 bond, address addr) internal pure returns (bool) {
574
+ return (last_history_hash == keccak256(abi.encodePacked(history_hash, answer, bond, addr, false)));
575
+ }
576
+
577
+ // The answered-too-soon version will override this with (answer != UNRESOLVED_ANSWER)
578
+ function _isBountyPayableOnAnswer(bytes32) internal pure virtual returns (bool) {
579
+ return true;
672
580
  }
673
581
 
674
582
  function _processHistoryItem(bytes32 question_id, bytes32 best_answer, uint256 queued_funds, address payee, address addr, uint256 bond, bytes32 answer) internal returns (uint256, address) {
@@ -678,7 +586,7 @@ contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20 {
678
586
  // They get the question bounty.
679
587
  payee = addr;
680
588
 
681
- if (best_answer != UNRESOLVED_ANSWER && questions[question_id].bounty > 0) {
589
+ if (questions[question_id].bounty > 0 && _isBountyPayableOnAnswer(best_answer)) {
682
590
  _payPayee(question_id, payee, questions[question_id].bounty);
683
591
  questions[question_id].bounty = 0;
684
592
  }
@@ -746,6 +654,40 @@ contract RealityETH_ERC20_v4_0 is BalanceHolder_ERC20, IRealityETHCore_ERC20 {
746
654
  withdraw();
747
655
  }
748
656
 
657
+ /// @notice Returns true if the supplied history is valid
658
+ /// @dev Caller must provide the answer history, in reverse order back to the item they want to check
659
+ /// @dev Not necessarily the entire history
660
+ /// @dev Useful for freezing an action once a bond is paid for a particular answer, without waiting for resolution
661
+ /// @dev Cannot be used after the question is finalized
662
+ /// @param question_id The ID of the question
663
+ /// @param history_hashes Second-last-to-first, the hash of each history entry. (Final one should be empty).
664
+ /// @param addrs Last-to-first, the address of each answerer
665
+ /// @param bonds Last-to-first, the bond supplied with each answer
666
+ /// @param answers Last-to-first, each answer supplied
667
+ function isHistoryOfUnfinalizedQuestionValid(
668
+ bytes32 question_id,
669
+ bytes32[] memory history_hashes,
670
+ address[] memory addrs,
671
+ uint256[] memory bonds,
672
+ bytes32[] memory answers
673
+ ) external view stateOpenOrPendingArbitration(question_id) returns (bool) {
674
+ bytes32 last_history_hash = questions[question_id].history_hash;
675
+
676
+ uint256 hist_len = history_hashes.length;
677
+ // Check for uneven length entries to make sure we validate all the inputs
678
+ if (addrs.length != hist_len || bonds.length != hist_len || answers.length != hist_len) {
679
+ return false;
680
+ }
681
+
682
+ for (uint256 i = 0; i < hist_len; i++) {
683
+ if (!_isHistoryInputValidForHash(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i])) {
684
+ return false;
685
+ }
686
+ last_history_hash = history_hashes[i];
687
+ }
688
+ return true;
689
+ }
690
+
749
691
  /// @notice Returns the questions's content hash, identifying the question content
750
692
  /// @param question_id The ID of the question
751
693
  function getContentHash(bytes32 question_id) public view returns (bytes32) {
@@ -327,7 +327,7 @@
327
327
  "symbol": "ETH",
328
328
  "decimals": 18
329
329
  },
330
- "network_name": "zksync-era-sepolia",
330
+ "network_name": "zksync-sepolia",
331
331
  "rpcUrls": [
332
332
  "https://sepolia.era.zksync.dev"
333
333
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reality.eth/contracts",
3
- "version": "3.0.48",
3
+ "version": "4.0.0-rc.0",
4
4
  "description": "Collection of smart contracts for the Realitio fact verification platform",
5
5
  "scripts": {
6
6
  "generate-chains": "node scripts/generate_chains_json.js",
@@ -34,7 +34,7 @@
34
34
  "url": "https://github.com/RealityETH/monorepo/issues"
35
35
  },
36
36
  "homepage": "https://reality.eth.link",
37
- "gitHead": "94611f2560bbff608c8c481d9ed6e65efba70159",
37
+ "gitHead": "03e4a48bc6ffcb5d327c5503fa1afb2eba3f97fb",
38
38
  "dependencies": {
39
39
  "ethers": "^5.6.8"
40
40
  }
@@ -43,12 +43,27 @@ if VERNUM >= 2.1:
43
43
  else:
44
44
  CLAIM_FEE = 0
45
45
 
46
+ if VERNUM == 3.0:
47
+ IS_ANSWERED_TOO_SOON_SUPPORTED = True
48
+ elif VERNUM >= 4.0:
49
+ if "Reopenable" in REALITYETH_CONTRACT:
50
+ IS_ANSWERED_TOO_SOON_SUPPORTED = True
51
+ else:
52
+ IS_ANSWERED_TOO_SOON_SUPPORTED = False
53
+ else:
54
+ # TODO: Add special version
55
+ IS_ANSWERED_TOO_SOON_SUPPORTED = False
56
+
46
57
  if VERNUM >= 4.0:
47
58
  IS_COMMIT_REVEAL_SUPPORTED = False
59
+ IS_PROVE_HISTORY_SUPPORTED = True
48
60
  else:
49
61
  IS_COMMIT_REVEAL_SUPPORTED = True
62
+ IS_PROVE_HISTORY_SUPPORTED = False
50
63
 
51
64
  print("IS_COMMIT_REVEAL_SUPPORTED is "+str(IS_COMMIT_REVEAL_SUPPORTED))
65
+ print("IS_ANSWERED_TOO_SOON_SUPPORTED is "+str(IS_ANSWERED_TOO_SOON_SUPPORTED))
66
+ print("CLAIM_FEE is "+str(CLAIM_FEE))
52
67
 
53
68
  DEPLOY_GAS = 8000000
54
69
 
@@ -2406,7 +2421,7 @@ class TestRealitio(TestCase):
2406
2421
  @unittest.skipIf(WORKING_ONLY, "Not under construction")
2407
2422
  def test_reopen_question(self):
2408
2423
 
2409
- if VERNUM < 3.0:
2424
+ if not IS_ANSWERED_TOO_SOON_SUPPORTED:
2410
2425
  print("Skipping test_reopen_question, not a feature of this contract")
2411
2426
  return
2412
2427
 
@@ -2552,7 +2567,7 @@ class TestRealitio(TestCase):
2552
2567
  @unittest.skipIf(WORKING_ONLY, "Not under construction")
2553
2568
  def test_too_soon_bounty(self):
2554
2569
 
2555
- if VERNUM < 3.0:
2570
+ if not IS_ANSWERED_TOO_SOON_SUPPORTED:
2556
2571
  print("Skipping test_reopen_question, not a feature of this contract")
2557
2572
  return
2558
2573
 
@@ -2590,8 +2605,8 @@ class TestRealitio(TestCase):
2590
2605
  print("Skipping test_too_soon_bonds_under_unrevealed_commit, contract does not support commit-reveal")
2591
2606
  return
2592
2607
 
2593
- if VERNUM < 3.0:
2594
- print("Skipping test_reopen_question, not a feature of this contract")
2608
+ if not IS_ANSWERED_TOO_SOON_SUPPORTED:
2609
+ print("Skipping test_too_soon_bonds_under_unrevealed_commit , not a feature of this contract")
2595
2610
  return
2596
2611
 
2597
2612
  k0 = self.web3.eth.accounts[0]
@@ -2624,13 +2639,74 @@ class TestRealitio(TestCase):
2624
2639
 
2625
2640
  self.assertEqual(self.rc0.functions.balanceOf(k3).call(), claimable)
2626
2641
 
2642
+ #@unittest.skipIf(WORKING_ONLY, "Not under construction")
2643
+ def test_is_history_of_unfinalized_question_valid(self):
2644
+
2645
+ if not IS_PROVE_HISTORY_SUPPORTED:
2646
+ print("Skipping test_is_history_of_unfinalized_question_valid, not supported by this version")
2647
+ return
2648
+
2649
+ k2 = self.web3.eth.accounts[2]
2650
+ k3 = self.web3.eth.accounts[3]
2651
+ k4 = self.web3.eth.accounts[4]
2652
+
2653
+ if ERC20:
2654
+ self._issueTokens(k3, 100000, 50000)
2655
+ self._issueTokens(k4, 100000, 50000)
2656
+
2657
+ st = None
2658
+ st = self.submitAnswerReturnUpdatedState( st, self.question_id, 1001, 0, 20, k3)
2659
+ st = self.submitAnswerReturnUpdatedState( st, self.question_id, 1002, 20, 40, k3)
2660
+ st = self.submitAnswerReturnUpdatedState( st, self.question_id, 1001, 40, 80, k4)
2661
+ st = self.submitAnswerReturnUpdatedState( st, self.question_id, 1004, 80, 160, k3)
2662
+ st = self.submitAnswerReturnUpdatedState( st, self.question_id, 1003, 160, 320, k4)
2663
+ st = self.submitAnswerReturnUpdatedState( st, self.question_id, 1001, 320, 640, k4)
2664
+
2665
+ hashes = st['hash']
2666
+ addrs = st['addr']
2667
+ bonds = st['bond']
2668
+ answers = st['answer']
2627
2669
 
2628
-
2670
+ self.assertTrue(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes, addrs, bonds, answers).call())
2629
2671
 
2672
+ bad_hashes = hashes.copy()
2673
+ bad_hashes[2] = bad_hashes[1]
2674
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, bad_hashes, addrs, bonds, answers).call())
2630
2675
 
2676
+ bad_addrs = addrs.copy()
2677
+ bad_addrs[2] = k2
2678
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes, bad_addrs, bonds, answers).call())
2631
2679
 
2680
+ bad_bonds = bonds.copy()
2681
+ bad_bonds[1] = 987
2682
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes, addrs, bad_bonds, answers).call())
2632
2683
 
2684
+ bad_answers = answers.copy()
2685
+ bad_answers[1] = to_answer_for_contract(987)
2686
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes, addrs, bonds, bad_answers).call())
2633
2687
 
2688
+ # Supplying just the end of the list (lowest-bond answers) will fail
2689
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[3:], addrs[3:], bonds[3:], answers[3:]).call())
2690
+
2691
+ # Supplying just the start of the list (highest-bond answers) will work
2692
+ self.assertTrue(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:3], addrs[:3], bonds[:3], answers[:3]).call())
2693
+
2694
+ # Any unevenness in the length of parameters passed will fail
2695
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:4], addrs[:3], bonds[:3], answers[:3]).call())
2696
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:3], addrs[:4], bonds[:3], answers[:3]).call())
2697
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:3], addrs[:3], bonds[:4], answers[:3]).call())
2698
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:3], addrs[:3], bonds[:3], answers[:4]).call())
2699
+
2700
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:2], addrs[:3], bonds[:3], answers[:3]).call())
2701
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:3], addrs[:2], bonds[:3], answers[:3]).call())
2702
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:3], addrs[:3], bonds[:2], answers[:3]).call())
2703
+ self.assertFalse(self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, hashes[:3], addrs[:3], bonds[:3], answers[:2]).call())
2704
+
2705
+ self._advance_clock(33)
2706
+
2707
+ # Once we finalize this will revert
2708
+ with self.assertRaises(TransactionFailed):
2709
+ self.rc0.functions.isHistoryOfUnfinalizedQuestionValid(self.question_id, st['hash'], st['addr'], st['bond'], st['answer']).transact()
2634
2710
 
2635
2711
  if __name__ == '__main__':
2636
2712
  main()