-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathOracle.sol
290 lines (260 loc) · 9.43 KB
/
Oracle.sol
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
pragma abicoder v2;
import "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
import "../interfaces/Types.sol";
import "../interfaces/IOracleMaster.sol";
import "../interfaces/IPushable.sol";
import "./utils/ReportUtils.sol";
contract Oracle is ReentrancyGuard {
using ReportUtils for uint256;
event ReportingCleared();
event ReportSubmitted(uint128 eraId, uint128 eraNonce, address oracle);
// Current era report hashes
uint256[] internal currentReportVariants;
// Current era reports
Types.OracleData[] private currentReports;
// Then oracle member push report, its bit is set
uint256 internal currentReportBitmask;
// inactivity cover contract address
address payable[] public PUSHABLES;
// oracle master contract address
address public ORACLE_MASTER;
// current era nonce
uint128 public eraNonce;
// Current report variant hash by veto oracle member
uint256 internal currentVetoReportVariant;
// A historical map of what block hash was used to construct the oracle reports
// Used by oracles to go back in time and find the delegators of members that have unregistered and kicked out all their delegators
mapping(uint128 => bytes32) public erasToBlockHashes;
// Allows function calls only from OracleMaster
modifier onlyOracleMaster() {
require(msg.sender == ORACLE_MASTER);
_;
}
/**
* @notice Initialize oracle contract
* @param _oracleMaster oracle master address
*/
function initialize(
address _oracleMaster,
address payable _pushable
) external {
require(
ORACLE_MASTER == address(0) && _oracleMaster != address(0),
"ALREADY_INITIALIZED"
);
ORACLE_MASTER = _oracleMaster;
PUSHABLES.push(_pushable);
}
/// ***************** FUNCTIONS CALLABLE BY ORACLE MASTER *****************
/**
@notice Accept oracle report data, allowed to call only by oracle master contract
@dev A report is considered and final and is pushed to the cover contract when
A) Q number of oracles have reported exactly the same report, where Q = quorum
B) the veto account has submitted a report and it agrees with the quorum report (immediately or when quorum is reached), OR the veto account is disabled
@param _index oracle member index
@param _quorum the minimum number of voted oracle members to accept a variant
@param _eraId current era id
@param _staking report data
*/
function reportPara(
uint256 _index,
uint256 _quorum,
uint128 _eraId,
uint128 _eraNonce,
Types.OracleData calldata _staking,
address _oracleCollator,
bool veto,
bool vetoDisabled,
bool newEra
) external onlyOracleMaster {
if (newEra) {
_clearReporting();
}
{
uint256 mask = 1 << _index;
uint256 reportBitmask = currentReportBitmask;
require(reportBitmask & mask == 0, "OR: ALREADY_SUBMITTED");
currentReportBitmask = (reportBitmask | mask);
require(_eraNonce == eraNonce, "OR: INV_NONCE");
}
// convert staking report into 31 byte hash. The last byte is used for vote counting
uint256 variant = uint256(keccak256(abi.encode(_staking))) &
ReportUtils.COUNT_OUTMASK;
if (veto) {
currentVetoReportVariant = variant;
}
uint256 i = 0;
uint256 _length = currentReportVariants.length;
// iterate on all report variants we already have, limited by the oracle members maximum
while (i < _length && currentReportVariants[i].isDifferent(variant)) {
++i;
}
if (i < _length) {
if (currentReportVariants[i].getCount() + 1 >= _quorum) {
bool vetoAddressHasVoted = currentVetoReportVariant == 0;
bool vetoed = currentReportVariants[i].isDifferent(currentVetoReportVariant);
if ((vetoAddressHasVoted && !vetoed) || vetoDisabled) {
_push(_eraId, _staking, _oracleCollator);
}
} else {
++currentReportVariants[i];
// increment variant counter, see ReportUtils for details
}
} else if (_quorum == 1) {
_push(_eraId, _staking, _oracleCollator);
} else {
currentReportVariants.push(variant + 1);
currentReports.push(_staking);
}
emit ReportSubmitted(_eraId, _eraNonce, _oracleCollator);
}
/**
* @notice Change quorum threshold, allowed to call only by oracle master contract
* @dev Method can trigger to pushing data to ledger if quorum threshold decreased and
now for contract already reached new threshold.
* @param _quorum new quorum threshold
* @param _eraId current era id
*/
function softenQuorum(
uint8 _quorum,
uint128 _eraId
) external onlyOracleMaster {
(bool isQuorum, uint256 reportIndex) = _getQuorumReport(_quorum);
if (isQuorum) {
Types.OracleData memory report = _getStakeReport(reportIndex);
_push(_eraId, report, address(0)); // pushing the zero address will deactivate gas cost refund
}
}
function addRemovePushable(
address payable _pushable,
bool _toAdd
) external onlyOracleMaster {
if (_toAdd) {
PUSHABLES.push(_pushable);
} else {
uint index;
uint256 length = PUSHABLES.length;
for (uint256 i = 0; i < length;) {
if (PUSHABLES[i] == _pushable) {
index = i;
break;
}
unchecked {
++i;
}
}
uint256 last = length - 1;
if (index != last) PUSHABLES[index] = PUSHABLES[last];
PUSHABLES.pop();
}
}
/**
* @notice Returns true if member is already reported
* @param _index oracle member index
* @return is reported indicator
*/
function isReported(
uint256 _index
) external view onlyOracleMaster returns (uint128, bool) {
bool reported = (currentReportBitmask & (1 << _index)) != 0;
return (eraNonce, reported);
}
/// ***************** INTERNAL FUNCTIONS *****************
/**
* @notice Clear data about current reporting, allowed to call only by oracle master contract
*/
function clearReporting() external onlyOracleMaster {
_clearReporting();
eraNonce++;
}
/**
* @notice Returns report by given index
* @param _index oracle member index
* @return staking report data
*/
function _getStakeReport(
uint256 _index
) internal view returns (Types.OracleData memory staking) {
require(_index < currentReports.length, "OR: OUT_OF_INDEX");
return currentReports[_index];
}
/**
* @notice Clear data about current reporting
*/
function _clearReporting() internal {
delete currentReportBitmask; // set to 0
delete currentReportVariants;
delete currentReports;
delete currentVetoReportVariant; // set to 0
emit ReportingCleared();
}
/**
* @notice Push data to all pushable contracts
*/
function _push(
uint128 _eraId,
Types.OracleData memory report,
address _oracleCollator
) internal {
erasToBlockHashes[_eraId] = report.blockHash;
uint256 length = PUSHABLES.length;
for (uint256 i = 0; i < length;) {
if (PUSHABLES[i] == address(0)) {
unchecked {
++i;
}
continue;
}
IPushable(PUSHABLES[i]).pushData(_eraId, report, _oracleCollator);
unchecked {
++i;
}
}
_clearReporting();
if (report.finalize) {
eraNonce++;
}
}
/**
@notice Return whether the `_quorum` is reached and the final report can be pushed
@dev From LIDO liquid KSM contract
*/
function _getQuorumReport(
uint256 _quorum
) internal view returns (bool, uint256) {
// check most frequent cases first: all reports are the same or no reports yet
uint256 _length = currentReportVariants.length;
if (_length == 1) {
return (currentReportVariants[0].getCount() >= _quorum, 0);
} else if (_length == 0) {
return (false, type(uint256).max);
}
// if more than 2 kinds of reports exist, choose the most frequent
uint256 maxind = 0;
uint256 repeat = 0;
uint16 maxval = 0;
uint16 cur = 0;
for (uint256 i = 0; i < _length;) {
cur = currentReportVariants[i].getCount();
if (cur >= maxval) {
if (cur == maxval) {
++repeat;
} else {
maxind = i;
maxval = cur;
repeat = 0;
}
}
unchecked {
++i;
}
}
return (maxval >= _quorum && repeat == 0, maxind);
}
function getReportBlockHash(uint128 _eraId) external view returns(bytes32) {
return erasToBlockHashes[_eraId];
}
}