@reality.eth/contracts 3.0.48 → 4.0.0-rc.1
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/RealityETHFreezableExample_ERC20-4.0.abi.json +1 -0
- 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/RealityETHFreezableExample_ERC20-4.0.bin +1 -0
- 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/IBalanceHolder.sol +1 -0
- package/development/contracts/IRealityETH.sol +6 -2
- package/development/contracts/{IRealityETHCore.sol → IRealityETHCore_Common.sol} +4 -23
- package/development/contracts/IRealityETHCore_ERC20.sol +3 -124
- package/development/contracts/IRealityETHCore_Native.sol +12 -0
- package/development/contracts/IRealityETHCreateTemplateAndAskQuestion.sol +8 -0
- 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/IRealityETH_ERC20.sol +5 -1
- package/development/contracts/RealityETH-4.0.sol +12 -614
- package/development/contracts/RealityETHCore_Common.sol +535 -0
- package/development/contracts/RealityETHFreezableExample_ERC20-4.0.sol +17 -0
- package/development/contracts/RealityETHFreezable_ERC20.sol +39 -0
- 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 +13 -614
- package/generated/chains.json +1 -1
- package/package.json +2 -2
- package/tests/python/test.py +186 -5
- package/development/contracts/BalanceHolder_ERC20.sol +0 -21
- package/development/contracts/IBalanceHolder_ERC20.sol +0 -11
|
@@ -0,0 +1,535 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
|
|
3
|
+
pragma solidity ^0.8.20;
|
|
4
|
+
|
|
5
|
+
import {IRealityETHCore_Common} from "./IRealityETHCore_Common.sol";
|
|
6
|
+
import {IRealityETHHistoryVerification} from "./IRealityETHHistoryVerification.sol";
|
|
7
|
+
|
|
8
|
+
// solhint-disable-next-line contract-name-camelcase
|
|
9
|
+
abstract contract RealityETHCore_Common is IRealityETHCore_Common, IRealityETHHistoryVerification {
|
|
10
|
+
// Proportion withheld when you claim an earlier bond.
|
|
11
|
+
uint256 private constant BOND_CLAIM_FEE_PROPORTION = 40; // One 40th ie 2.5%
|
|
12
|
+
|
|
13
|
+
uint256 private nextTemplateID = 0;
|
|
14
|
+
mapping(uint256 => uint256) public templates;
|
|
15
|
+
mapping(uint256 => bytes32) public template_hashes;
|
|
16
|
+
mapping(bytes32 => Question) public questions;
|
|
17
|
+
mapping(bytes32 => Claim) public question_claims;
|
|
18
|
+
mapping(address => uint256) public arbitrator_question_fees;
|
|
19
|
+
|
|
20
|
+
mapping(address => uint256) public balanceOf;
|
|
21
|
+
|
|
22
|
+
modifier onlyArbitrator(bytes32 question_id) {
|
|
23
|
+
if (msg.sender != questions[question_id].arbitrator) revert MsgSenderMustBeArbitrator();
|
|
24
|
+
_;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
modifier stateAny() {
|
|
28
|
+
_;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
modifier notFrozen() virtual {
|
|
32
|
+
_;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
modifier stateNotCreated(bytes32 question_id) {
|
|
36
|
+
if (questions[question_id].timeout != 0) revert QuestionMustNotExist();
|
|
37
|
+
_;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
modifier stateOpen(bytes32 question_id) {
|
|
41
|
+
if (questions[question_id].timeout == 0) revert QuestionMustExist();
|
|
42
|
+
if (questions[question_id].is_pending_arbitration) revert QuestionMustNotBePendingArbitration();
|
|
43
|
+
uint32 finalize_ts = questions[question_id].finalize_ts;
|
|
44
|
+
if (finalize_ts != 0 && finalize_ts <= uint32(block.timestamp)) revert FinalizationDeadlineMustNotHavePassed();
|
|
45
|
+
uint32 opening_ts = questions[question_id].opening_ts;
|
|
46
|
+
if (opening_ts != 0 && opening_ts > uint32(block.timestamp)) revert OpeningDateMustHavePassed();
|
|
47
|
+
_;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
modifier statePendingArbitration(bytes32 question_id) {
|
|
51
|
+
if (!questions[question_id].is_pending_arbitration) revert QuestionMustBePendingArbitration();
|
|
52
|
+
_;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
modifier stateOpenOrPendingArbitration(bytes32 question_id) {
|
|
56
|
+
if (questions[question_id].timeout == 0) revert QuestionMustExist();
|
|
57
|
+
uint32 finalize_ts = questions[question_id].finalize_ts;
|
|
58
|
+
if (finalize_ts != 0 && finalize_ts <= uint32(block.timestamp)) revert FinalizationDealineMustNotHavePassed();
|
|
59
|
+
uint32 opening_ts = questions[question_id].opening_ts;
|
|
60
|
+
if (opening_ts != 0 && opening_ts > uint32(block.timestamp)) revert OpeningDateMustHavePassed();
|
|
61
|
+
_;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
modifier stateFinalized(bytes32 question_id) {
|
|
65
|
+
if (!isFinalized(question_id)) revert QuestionMustBeFinalized();
|
|
66
|
+
_;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
modifier bondMustDoubleAndMatchMinimum(bytes32 question_id, uint256 tokens) {
|
|
70
|
+
if (tokens == 0) revert BondMustBePositive();
|
|
71
|
+
uint256 current_bond = questions[question_id].bond;
|
|
72
|
+
if (current_bond == 0) {
|
|
73
|
+
if (tokens < (questions[question_id].min_bond)) revert BondMustExceedTheMinimum();
|
|
74
|
+
} else {
|
|
75
|
+
if (tokens < (current_bond * 2)) revert BondMustBeDoubleAtLeastPreviousBond();
|
|
76
|
+
}
|
|
77
|
+
_;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
modifier previousBondMustNotBeatMaxPrevious(bytes32 question_id, uint256 max_previous) {
|
|
81
|
+
if (max_previous > 0) {
|
|
82
|
+
if (questions[question_id].bond > max_previous) revert BondMustExceedMax_Previous();
|
|
83
|
+
}
|
|
84
|
+
_;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* solhint-disable quotes */
|
|
88
|
+
/// @notice Constructor, sets up some initial templates
|
|
89
|
+
/// @dev Creates some generalized templates for different question types used in the DApp.
|
|
90
|
+
constructor() {
|
|
91
|
+
createTemplate('{"title": "%s", "type": "bool", "category": "%s", "lang": "%s"}');
|
|
92
|
+
createTemplate('{"title": "%s", "type": "uint", "decimals": 18, "category": "%s", "lang": "%s"}');
|
|
93
|
+
createTemplate('{"title": "%s", "type": "single-select", "outcomes": [%s], "category": "%s", "lang": "%s"}');
|
|
94
|
+
createTemplate('{"title": "%s", "type": "multiple-select", "outcomes": [%s], "category": "%s", "lang": "%s"}');
|
|
95
|
+
createTemplate('{"title": "%s", "type": "datetime", "category": "%s", "lang": "%s"}');
|
|
96
|
+
}
|
|
97
|
+
/* solhint-enable quotes */
|
|
98
|
+
|
|
99
|
+
/// @notice Function for arbitrator to set an optional per-question fee.
|
|
100
|
+
/// @dev The per-question fee, charged when a question is asked, is intended as an anti-spam measure.
|
|
101
|
+
/// @param fee The fee to be charged by the arbitrator when a question is asked
|
|
102
|
+
function setQuestionFee(uint256 fee) external stateAny notFrozen {
|
|
103
|
+
arbitrator_question_fees[msg.sender] = fee;
|
|
104
|
+
emit LogSetQuestionFee(msg.sender, fee);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// @notice Create a reusable template, which should be a JSON document.
|
|
108
|
+
/// Placeholders should use gettext() syntax, eg %s.
|
|
109
|
+
/// @dev Template data is only stored in the event logs, but its block number is kept in contract storage.
|
|
110
|
+
/// @param content The template content
|
|
111
|
+
/// @return The ID of the newly-created template, which is created sequentially.
|
|
112
|
+
function createTemplate(string memory content) public stateAny notFrozen returns (uint256) {
|
|
113
|
+
uint256 id = nextTemplateID;
|
|
114
|
+
templates[id] = block.number;
|
|
115
|
+
template_hashes[id] = keccak256(abi.encodePacked(content));
|
|
116
|
+
emit LogNewTemplate(id, msg.sender, content);
|
|
117
|
+
nextTemplateID = id + 1;
|
|
118
|
+
return id;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _askQuestion(bytes32 question_id, bytes32 content_hash, address arbitrator, uint32 timeout, uint32 opening_ts, uint256 min_bond, uint256 tokens) internal stateNotCreated(question_id) notFrozen {
|
|
122
|
+
// A timeout of 0 makes no sense, and we will use this to check existence
|
|
123
|
+
if (timeout == 0) revert TimeoutMustBePositive();
|
|
124
|
+
if (timeout >= 365 days) revert TimeoutMustBeLessThan365Days();
|
|
125
|
+
|
|
126
|
+
uint256 bounty = tokens;
|
|
127
|
+
|
|
128
|
+
// The arbitrator can set a fee for asking a question.
|
|
129
|
+
// This is intended as an anti-spam defence.
|
|
130
|
+
// The fee is waived if the arbitrator is asking the question.
|
|
131
|
+
// This allows them to set an impossibly high fee and make users proxy the question through them.
|
|
132
|
+
// This would allow more sophisticated pricing, question whitelisting etc.
|
|
133
|
+
if (arbitrator != address(0) && msg.sender != arbitrator) {
|
|
134
|
+
uint256 question_fee = arbitrator_question_fees[arbitrator];
|
|
135
|
+
if (bounty < question_fee) revert TokensProvidedMustCoverQuestionFee();
|
|
136
|
+
bounty = bounty - question_fee;
|
|
137
|
+
balanceOf[arbitrator] = balanceOf[arbitrator] + question_fee;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
questions[question_id].content_hash = content_hash;
|
|
141
|
+
questions[question_id].arbitrator = arbitrator;
|
|
142
|
+
questions[question_id].opening_ts = opening_ts;
|
|
143
|
+
questions[question_id].timeout = timeout;
|
|
144
|
+
|
|
145
|
+
if (bounty > 0) {
|
|
146
|
+
questions[question_id].bounty = bounty;
|
|
147
|
+
emit LogFundAnswerBounty(question_id, bounty, bounty, msg.sender);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (min_bond > 0) {
|
|
151
|
+
questions[question_id].min_bond = min_bond;
|
|
152
|
+
emit LogMinimumBond(question_id, min_bond);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _addAnswerToHistory(bytes32 question_id, bytes32 answer, address answerer, uint256 bond) internal {
|
|
157
|
+
bytes32 new_history_hash = keccak256(abi.encodePacked(questions[question_id].history_hash, answer, bond, answerer, false));
|
|
158
|
+
|
|
159
|
+
// Update the current bond level, if there's a bond (ie anything except arbitration)
|
|
160
|
+
if (bond > 0) {
|
|
161
|
+
questions[question_id].bond = bond;
|
|
162
|
+
}
|
|
163
|
+
questions[question_id].history_hash = new_history_hash;
|
|
164
|
+
|
|
165
|
+
emit LogNewAnswer(answer, question_id, new_history_hash, answerer, bond, block.timestamp, false);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function _updateCurrentAnswer(bytes32 question_id, bytes32 answer) internal {
|
|
169
|
+
questions[question_id].best_answer = answer;
|
|
170
|
+
questions[question_id].finalize_ts = uint32(block.timestamp) + questions[question_id].timeout;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Like _updateCurrentAnswer but without advancing the timeout
|
|
174
|
+
function _updateCurrentAnswerByArbitrator(bytes32 question_id, bytes32 answer) internal {
|
|
175
|
+
questions[question_id].best_answer = answer;
|
|
176
|
+
questions[question_id].finalize_ts = uint32(block.timestamp);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// @notice Notify the contract that the arbitrator has been paid for a question, freezing it pending their decision.
|
|
180
|
+
/// @dev The arbitrator contract is trusted to only call this if they've been paid, and tell us who paid them.
|
|
181
|
+
/// @param question_id The ID of the question
|
|
182
|
+
/// @param requester The account that requested arbitration
|
|
183
|
+
/// @param max_previous If specified, reverts if a bond higher than this was submitted after you sent your transaction.
|
|
184
|
+
function notifyOfArbitrationRequest(
|
|
185
|
+
bytes32 question_id,
|
|
186
|
+
address requester,
|
|
187
|
+
uint256 max_previous
|
|
188
|
+
) external onlyArbitrator(question_id) stateOpen(question_id) previousBondMustNotBeatMaxPrevious(question_id, max_previous) notFrozen {
|
|
189
|
+
if (questions[question_id].finalize_ts <= 0) revert QuestionMustAlreadyHaveAnAnswerWhenArbitrationIsRequested();
|
|
190
|
+
questions[question_id].is_pending_arbitration = true;
|
|
191
|
+
emit LogNotifyOfArbitrationRequest(question_id, requester);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/// @notice Cancel a previously-requested arbitration and extend the timeout
|
|
195
|
+
/// @dev Useful when doing arbitration across chains that can't be requested atomically
|
|
196
|
+
/// @param question_id The ID of the question
|
|
197
|
+
function cancelArbitration(bytes32 question_id) external onlyArbitrator(question_id) statePendingArbitration(question_id) notFrozen {
|
|
198
|
+
questions[question_id].is_pending_arbitration = false;
|
|
199
|
+
questions[question_id].finalize_ts = uint32(block.timestamp) + questions[question_id].timeout;
|
|
200
|
+
emit LogCancelArbitration(question_id);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// @notice Submit the answer for a question, for use by the arbitrator.
|
|
204
|
+
/// @dev Doesn't require (or allow) a bond.
|
|
205
|
+
/// If the current final answer is correct, the account should be whoever submitted it.
|
|
206
|
+
/// If the current final answer is wrong, the account should be whoever paid for arbitration.
|
|
207
|
+
/// However, the answerer stipulations are not enforced by the contract.
|
|
208
|
+
/// @param question_id The ID of the question
|
|
209
|
+
/// @param answer The answer, encoded into bytes32
|
|
210
|
+
/// @param answerer The account credited with this answer for the purpose of bond claims
|
|
211
|
+
function submitAnswerByArbitrator(bytes32 question_id, bytes32 answer, address answerer) public onlyArbitrator(question_id) statePendingArbitration(question_id) notFrozen {
|
|
212
|
+
if (answerer == address(0)) revert AnswererMustBeProvided();
|
|
213
|
+
emit LogFinalize(question_id, answer);
|
|
214
|
+
|
|
215
|
+
questions[question_id].is_pending_arbitration = false;
|
|
216
|
+
_addAnswerToHistory(question_id, answer, answerer, 0);
|
|
217
|
+
_updateCurrentAnswerByArbitrator(question_id, answer);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/// @notice Submit the answer for a question, for use by the arbitrator, working out the appropriate winner based on the last answer details.
|
|
221
|
+
/// @dev Doesn't require (or allow) a bond.
|
|
222
|
+
/// @param question_id The ID of the question
|
|
223
|
+
/// @param answer The answer, encoded into bytes32
|
|
224
|
+
/// @param payee_if_wrong The account to by credited as winner if the last answer given is wrong, usually the account that paid the arbitrator
|
|
225
|
+
/// @param last_history_hash The history hash before the final one
|
|
226
|
+
/// @param last_answer The last answer given
|
|
227
|
+
/// @param last_answerer The address that supplied the last answer
|
|
228
|
+
function assignWinnerAndSubmitAnswerByArbitrator(bytes32 question_id, bytes32 answer, address payee_if_wrong, bytes32 last_history_hash, bytes32 last_answer, address last_answerer) external {
|
|
229
|
+
if (!_isHistoryInputValidForHash(questions[question_id].history_hash, last_history_hash, last_answer, questions[question_id].bond, last_answerer)) {
|
|
230
|
+
revert HistoryInputProvidedDidNotMatchTheExpectedHash();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
address payee = (questions[question_id].best_answer == answer) ? last_answerer : payee_if_wrong;
|
|
234
|
+
submitAnswerByArbitrator(question_id, answer, payee);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/// @notice Report whether the answer to the specified question is finalized
|
|
238
|
+
/// @param question_id The ID of the question
|
|
239
|
+
/// @return Return true if finalized
|
|
240
|
+
function isFinalized(bytes32 question_id) public view virtual returns (bool) {
|
|
241
|
+
uint32 finalize_ts = questions[question_id].finalize_ts;
|
|
242
|
+
return (!questions[question_id].is_pending_arbitration && (finalize_ts > 0) && (finalize_ts <= uint32(block.timestamp)));
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/// @notice (Deprecated) Return the final answer to the specified question, or revert if there isn't one
|
|
246
|
+
/// @param question_id The ID of the question
|
|
247
|
+
/// @return The answer formatted as a bytes32
|
|
248
|
+
function getFinalAnswer(bytes32 question_id) external view stateFinalized(question_id) returns (bytes32) {
|
|
249
|
+
return questions[question_id].best_answer;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// @notice Return the final answer to the specified question, or revert if there isn't one
|
|
253
|
+
/// @param question_id The ID of the question
|
|
254
|
+
/// @return The answer formatted as a bytes32
|
|
255
|
+
function resultFor(bytes32 question_id) public view stateFinalized(question_id) returns (bytes32) {
|
|
256
|
+
return questions[question_id].best_answer;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/// @notice Return the final answer to the specified question, provided it matches the specified criteria.
|
|
260
|
+
/// @dev Reverts if the question is not finalized, or if it does not match the specified criteria.
|
|
261
|
+
/// @param question_id The ID of the question
|
|
262
|
+
/// @param content_hash The hash of the question content (template ID + opening time + question parameter string)
|
|
263
|
+
/// @param arbitrator The arbitrator chosen for the question (regardless of whether they are asked to arbitrate)
|
|
264
|
+
/// @param min_timeout The timeout set in the initial question settings must be this high or higher
|
|
265
|
+
/// @param min_bond The bond sent with the final answer must be this high or higher
|
|
266
|
+
/// @return The answer formatted as a bytes32
|
|
267
|
+
function getFinalAnswerIfMatches(bytes32 question_id, bytes32 content_hash, address arbitrator, uint32 min_timeout, uint256 min_bond) external view stateFinalized(question_id) returns (bytes32) {
|
|
268
|
+
if (content_hash != questions[question_id].content_hash) revert ContentHashMustMatch();
|
|
269
|
+
if (arbitrator != questions[question_id].arbitrator) revert ArbitratorMustMatch();
|
|
270
|
+
if (min_timeout > questions[question_id].timeout) revert TimeoutMustBeLongEnough();
|
|
271
|
+
if (min_bond > questions[question_id].bond) revert BondMustBeHighEnough();
|
|
272
|
+
return questions[question_id].best_answer;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// @notice Assigns the winnings (bounty and bonds) to everyone who gave the accepted answer
|
|
276
|
+
/// Caller must provide the answer history, in reverse order
|
|
277
|
+
/// @dev Works up the chain and assign bonds to the person who gave the right answer
|
|
278
|
+
/// If someone gave the winning answer earlier, they must get paid from the higher bond
|
|
279
|
+
/// That means we can't pay out the bond added at n until we have looked at n-1
|
|
280
|
+
/// The first answer is authenticated by checking against the stored history_hash.
|
|
281
|
+
/// One of the inputs to history_hash is the history_hash before it, so we use that to authenticate the next entry, etc
|
|
282
|
+
/// Once we get to a null hash we'll know we're done and there are no more answers.
|
|
283
|
+
/// Usually you would call the whole thing in a single transaction, but if not then the data is persisted to pick up later.
|
|
284
|
+
/// @param question_id The ID of the question
|
|
285
|
+
/// @param history_hashes Second-last-to-first, the hash of each history entry. (Final one should be empty).
|
|
286
|
+
/// @param addrs Last-to-first, the address of each answerer
|
|
287
|
+
/// @param bonds Last-to-first, the bond supplied with each answer
|
|
288
|
+
/// @param answers Last-to-first, each answer supplied
|
|
289
|
+
function claimWinnings(bytes32 question_id, bytes32[] memory history_hashes, address[] memory addrs, uint256[] memory bonds, bytes32[] memory answers) public stateFinalized(question_id) {
|
|
290
|
+
if (history_hashes.length == 0) revert AtLeastOneHistoryHashEntryMustBeProvided();
|
|
291
|
+
|
|
292
|
+
// These are only set if we split our claim over multiple transactions.
|
|
293
|
+
address payee = question_claims[question_id].payee;
|
|
294
|
+
uint256 last_bond = question_claims[question_id].last_bond;
|
|
295
|
+
uint256 queued_funds = 0;
|
|
296
|
+
|
|
297
|
+
// Starts as the hash of the final answer submitted. It'll be cleared when we're done.
|
|
298
|
+
// If we're splitting the claim over multiple transactions, it'll be the hash where we left off last time
|
|
299
|
+
bytes32 last_history_hash = questions[question_id].history_hash;
|
|
300
|
+
|
|
301
|
+
bytes32 best_answer = questions[question_id].best_answer;
|
|
302
|
+
|
|
303
|
+
uint256 i;
|
|
304
|
+
for (i = 0; i < history_hashes.length; i++) {
|
|
305
|
+
if (!_isHistoryInputValidForHash(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i])) {
|
|
306
|
+
revert HistoryInputProvidedDidNotMatchTheExpectedHash();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
queued_funds = queued_funds + last_bond;
|
|
310
|
+
(queued_funds, payee) = _processHistoryItem(question_id, best_answer, queued_funds, payee, addrs[i], bonds[i], answers[i]);
|
|
311
|
+
|
|
312
|
+
// Line the bond up for next time, when it will be added to somebody's queued_funds
|
|
313
|
+
last_bond = bonds[i];
|
|
314
|
+
|
|
315
|
+
// Burn (just leave in contract balance) a fraction of all bonds except the final one.
|
|
316
|
+
// This creates a cost to increasing your own bond, which could be used to delay resolution maliciously
|
|
317
|
+
if (last_bond != questions[question_id].bond) {
|
|
318
|
+
last_bond = last_bond - last_bond / BOND_CLAIM_FEE_PROPORTION;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
last_history_hash = history_hashes[i];
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (last_history_hash != bytes32(0)) {
|
|
325
|
+
// We haven't yet got to the null hash (1st answer), ie the caller didn't supply the full answer chain.
|
|
326
|
+
// Persist the details so we can pick up later where we left off later.
|
|
327
|
+
|
|
328
|
+
// Pay out the latest payee, only keeping back last_bond which the next may have a claim on
|
|
329
|
+
_payPayee(question_id, payee, queued_funds);
|
|
330
|
+
|
|
331
|
+
question_claims[question_id].payee = payee;
|
|
332
|
+
question_claims[question_id].last_bond = last_bond;
|
|
333
|
+
} else {
|
|
334
|
+
// There is nothing left below us so the payee can keep what remains
|
|
335
|
+
_payPayee(question_id, payee, queued_funds + last_bond);
|
|
336
|
+
delete question_claims[question_id];
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
questions[question_id].history_hash = last_history_hash;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function _payPayee(bytes32 question_id, address payee, uint256 value) internal {
|
|
343
|
+
balanceOf[payee] = balanceOf[payee] + value;
|
|
344
|
+
emit LogClaim(question_id, payee, value);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function _isHistoryInputValidForHash(bytes32 last_history_hash, bytes32 history_hash, bytes32 answer, uint256 bond, address addr) internal pure returns (bool) {
|
|
348
|
+
return (last_history_hash == keccak256(abi.encodePacked(history_hash, answer, bond, addr, false)));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// The answered-too-soon version will override this with (answer != UNRESOLVED_ANSWER)
|
|
352
|
+
function _isBountyPayableOnAnswer(bytes32) internal pure virtual returns (bool) {
|
|
353
|
+
return true;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function _processHistoryItem(bytes32 question_id, bytes32 best_answer, uint256 queued_funds, address payee, address addr, uint256 bond, bytes32 answer) internal returns (uint256, address) {
|
|
357
|
+
if (answer == best_answer) {
|
|
358
|
+
if (payee == address(0)) {
|
|
359
|
+
// The entry is for the first payee we come to, ie the winner.
|
|
360
|
+
// They get the question bounty.
|
|
361
|
+
payee = addr;
|
|
362
|
+
|
|
363
|
+
if (questions[question_id].bounty > 0 && _isBountyPayableOnAnswer(best_answer)) {
|
|
364
|
+
_payPayee(question_id, payee, questions[question_id].bounty);
|
|
365
|
+
questions[question_id].bounty = 0;
|
|
366
|
+
}
|
|
367
|
+
} else if (addr != payee) {
|
|
368
|
+
// Answerer has changed, ie we found someone lower down who needs to be paid
|
|
369
|
+
|
|
370
|
+
// The lower answerer will take over receiving bonds from higher answerer.
|
|
371
|
+
// They should also be paid the takeover fee, which is set at a rate equivalent to their bond.
|
|
372
|
+
// (This is our arbitrary rule, to give consistent right-answerers a defence against high-rollers.)
|
|
373
|
+
|
|
374
|
+
// There should be enough for the fee, but if not, take what we have.
|
|
375
|
+
// There's an edge case involving weird arbitrator behaviour where we may be short.
|
|
376
|
+
uint256 answer_takeover_fee = (queued_funds >= bond) ? bond : queued_funds;
|
|
377
|
+
// Settle up with the old (higher-bonded) payee
|
|
378
|
+
_payPayee(question_id, payee, queued_funds - answer_takeover_fee);
|
|
379
|
+
|
|
380
|
+
// Now start queued_funds again for the new (lower-bonded) payee
|
|
381
|
+
payee = addr;
|
|
382
|
+
queued_funds = answer_takeover_fee;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return (queued_funds, payee);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/// @notice Convenience function to assign bounties/bonds for multiple questions in one go, then withdraw all your funds.
|
|
390
|
+
/// Caller must provide the answer history for each question, in reverse order
|
|
391
|
+
/// @dev Can be called by anyone to assign bonds/bounties, but funds are only withdrawn for the user making the call.
|
|
392
|
+
/// @param question_ids The IDs of the questions you want to claim for
|
|
393
|
+
/// @param lengths The number of history entries you will supply for each question ID
|
|
394
|
+
/// @param hist_hashes In a single list for all supplied questions, the hash of each history entry.
|
|
395
|
+
/// @param addrs In a single list for all supplied questions, the address of each answerer
|
|
396
|
+
/// @param bonds In a single list for all supplied questions, the bond supplied with each answer
|
|
397
|
+
/// @param answers In a single list for all supplied questions, each answer supplied
|
|
398
|
+
function claimMultipleAndWithdrawBalance(
|
|
399
|
+
bytes32[] memory question_ids,
|
|
400
|
+
uint256[] memory lengths,
|
|
401
|
+
bytes32[] memory hist_hashes,
|
|
402
|
+
address[] memory addrs,
|
|
403
|
+
uint256[] memory bonds,
|
|
404
|
+
bytes32[] memory answers
|
|
405
|
+
)
|
|
406
|
+
public
|
|
407
|
+
stateAny
|
|
408
|
+
notFrozen // The finalization checks are done in the claimWinnings function
|
|
409
|
+
{
|
|
410
|
+
uint256 qi;
|
|
411
|
+
uint256 i;
|
|
412
|
+
for (qi = 0; qi < question_ids.length; qi++) {
|
|
413
|
+
bytes32 qid = question_ids[qi];
|
|
414
|
+
uint256 ln = lengths[qi];
|
|
415
|
+
bytes32[] memory hh = new bytes32[](ln);
|
|
416
|
+
address[] memory ad = new address[](ln);
|
|
417
|
+
uint256[] memory bo = new uint256[](ln);
|
|
418
|
+
bytes32[] memory an = new bytes32[](ln);
|
|
419
|
+
uint256 j;
|
|
420
|
+
for (j = 0; j < ln; j++) {
|
|
421
|
+
hh[j] = hist_hashes[i];
|
|
422
|
+
ad[j] = addrs[i];
|
|
423
|
+
bo[j] = bonds[i];
|
|
424
|
+
an[j] = answers[i];
|
|
425
|
+
i++;
|
|
426
|
+
}
|
|
427
|
+
claimWinnings(qid, hh, ad, bo, an);
|
|
428
|
+
}
|
|
429
|
+
withdraw();
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/// @notice Returns true if the supplied history is valid
|
|
433
|
+
/// @dev Caller must provide the answer history, in reverse order back to the item they want to check
|
|
434
|
+
/// @dev Not necessarily the entire history
|
|
435
|
+
/// @dev Useful for freezing an action once a bond is paid for a particular answer, without waiting for resolution
|
|
436
|
+
/// @dev Cannot be used after the question is finalized
|
|
437
|
+
/// @param question_id The ID of the question
|
|
438
|
+
/// @param history_hashes Second-last-to-first, the hash of each history entry. (Final one should be empty).
|
|
439
|
+
/// @param addrs Last-to-first, the address of each answerer
|
|
440
|
+
/// @param bonds Last-to-first, the bond supplied with each answer
|
|
441
|
+
/// @param answers Last-to-first, each answer supplied
|
|
442
|
+
function isHistoryOfUnfinalizedQuestionValid(
|
|
443
|
+
bytes32 question_id,
|
|
444
|
+
bytes32[] memory history_hashes,
|
|
445
|
+
address[] memory addrs,
|
|
446
|
+
uint256[] memory bonds,
|
|
447
|
+
bytes32[] memory answers
|
|
448
|
+
) external view stateOpenOrPendingArbitration(question_id) returns (bool) {
|
|
449
|
+
bytes32 last_history_hash = questions[question_id].history_hash;
|
|
450
|
+
|
|
451
|
+
uint256 hist_len = history_hashes.length;
|
|
452
|
+
// Check for uneven length entries to make sure we validate all the inputs
|
|
453
|
+
if (addrs.length != hist_len || bonds.length != hist_len || answers.length != hist_len) {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
for (uint256 i = 0; i < hist_len; i++) {
|
|
458
|
+
if (!_isHistoryInputValidForHash(last_history_hash, history_hashes[i], answers[i], bonds[i], addrs[i])) {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
last_history_hash = history_hashes[i];
|
|
462
|
+
}
|
|
463
|
+
return true;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function withdraw() public virtual {}
|
|
467
|
+
|
|
468
|
+
/// @notice Returns the questions's content hash, identifying the question content
|
|
469
|
+
/// @param question_id The ID of the question
|
|
470
|
+
function getContentHash(bytes32 question_id) public view returns (bytes32) {
|
|
471
|
+
return questions[question_id].content_hash;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/// @notice Returns the arbitrator address for the question
|
|
475
|
+
/// @param question_id The ID of the question
|
|
476
|
+
function getArbitrator(bytes32 question_id) public view returns (address) {
|
|
477
|
+
return questions[question_id].arbitrator;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/// @notice Returns the timestamp when the question can first be answered
|
|
481
|
+
/// @param question_id The ID of the question
|
|
482
|
+
function getOpeningTS(bytes32 question_id) public view returns (uint32) {
|
|
483
|
+
return questions[question_id].opening_ts;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// @notice Returns the timeout in seconds used after each answer
|
|
487
|
+
/// @param question_id The ID of the question
|
|
488
|
+
function getTimeout(bytes32 question_id) public view returns (uint32) {
|
|
489
|
+
return questions[question_id].timeout;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/// @notice Returns the timestamp at which the question will be/was finalized
|
|
493
|
+
/// @param question_id The ID of the question
|
|
494
|
+
function getFinalizeTS(bytes32 question_id) public view returns (uint32) {
|
|
495
|
+
return questions[question_id].finalize_ts;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// @notice Returns whether the question is pending arbitration
|
|
499
|
+
/// @param question_id The ID of the question
|
|
500
|
+
function isPendingArbitration(bytes32 question_id) public view returns (bool) {
|
|
501
|
+
return questions[question_id].is_pending_arbitration;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// @notice Returns the current total unclaimed bounty
|
|
505
|
+
/// @dev Set back to zero once the bounty has been claimed
|
|
506
|
+
/// @param question_id The ID of the question
|
|
507
|
+
function getBounty(bytes32 question_id) public view returns (uint256) {
|
|
508
|
+
return questions[question_id].bounty;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/// @notice Returns the current best answer
|
|
512
|
+
/// @param question_id The ID of the question
|
|
513
|
+
function getBestAnswer(bytes32 question_id) public view returns (bytes32) {
|
|
514
|
+
return questions[question_id].best_answer;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/// @notice Returns the history hash of the question
|
|
518
|
+
/// @param question_id The ID of the question
|
|
519
|
+
/// @dev Updated on each answer, then rewound as each is claimed
|
|
520
|
+
function getHistoryHash(bytes32 question_id) public view returns (bytes32) {
|
|
521
|
+
return questions[question_id].history_hash;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/// @notice Returns the highest bond posted so far for a question
|
|
525
|
+
/// @param question_id The ID of the question
|
|
526
|
+
function getBond(bytes32 question_id) public view returns (uint256) {
|
|
527
|
+
return questions[question_id].bond;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/// @notice Returns the minimum bond that can answer the question
|
|
531
|
+
/// @param question_id The ID of the question
|
|
532
|
+
function getMinBond(bytes32 question_id) public view returns (uint256) {
|
|
533
|
+
return questions[question_id].min_bond;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
|
|
3
|
+
pragma solidity ^0.8.20;
|
|
4
|
+
|
|
5
|
+
import {RealityETHFreezable_ERC20} from "./RealityETHFreezable_ERC20.sol";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* This contract is designed for testing the abstract contract RealityETHFreezable_ERC20.
|
|
9
|
+
* It should not be used as is because anyone can call setFreezeTimestamp.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// solhint-disable-next-line contract-name-camelcase
|
|
13
|
+
contract RealityETHFreezableExample_ERC20_v4_0 is RealityETHFreezable_ERC20 {
|
|
14
|
+
function setFreezeTimestamp(uint32 _freeze_ts) external {
|
|
15
|
+
freeze_ts = _freeze_ts;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// SPDX-License-Identifier: GPL-3.0-only
|
|
2
|
+
|
|
3
|
+
pragma solidity ^0.8.20;
|
|
4
|
+
|
|
5
|
+
import {RealityETH_ERC20_v4_0} from "./RealityETH_ERC20-4.0.sol";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* This version of reality.eth provides the ability to freeze the entire contract.
|
|
9
|
+
* This is intended to be extended by the forkable reality.eth required by Backstop.
|
|
10
|
+
* It may be used for other purposes, but will need additional functionality to manage the permission for the freeze function.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// solhint-disable-next-line contract-name-camelcase
|
|
14
|
+
abstract contract RealityETHFreezable_ERC20 is RealityETH_ERC20_v4_0 {
|
|
15
|
+
/// @notice msg.sender must be arbitrator
|
|
16
|
+
error ContractIsFrozen();
|
|
17
|
+
|
|
18
|
+
// The timestamp when the contract was frozen.
|
|
19
|
+
uint32 public freeze_ts;
|
|
20
|
+
|
|
21
|
+
modifier notFrozen() override {
|
|
22
|
+
if (freeze_ts > 0) revert ContractIsFrozen();
|
|
23
|
+
_;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/// @notice Report whether the answer to the specified question is finalized
|
|
27
|
+
/// @param question_id The ID of the question
|
|
28
|
+
/// @return Return true if finalized
|
|
29
|
+
function isFinalized(bytes32 question_id) public view override returns (bool) {
|
|
30
|
+
// Nothing that finalizes after the freeze is finalized.
|
|
31
|
+
if (freeze_ts > 0 && questions[question_id].finalize_ts >= freeze_ts) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Questions that were queried before the freeze can be queried even after the freeze.
|
|
36
|
+
// However other operations such as claiming rewards are frozen by the notFrozen modifier.
|
|
37
|
+
return super.isFinalized(question_id);
|
|
38
|
+
}
|
|
39
|
+
}
|