/
Voting.sol
997 lines (873 loc) · 43.9 KB
/
Voting.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
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
// SPDX-License-Identifier: AGPL-3.0-only
pragma solidity ^0.8.0;
import "../../common/implementation/FixedPoint.sol";
import "../../common/implementation/Testable.sol";
import "../interfaces/FinderInterface.sol";
import "../interfaces/OracleInterface.sol";
import "../interfaces/OracleAncillaryInterface.sol";
import "../interfaces/VotingInterface.sol";
import "../interfaces/VotingAncillaryInterface.sol";
import "../interfaces/IdentifierWhitelistInterface.sol";
import "./Registry.sol";
import "./ResultComputation.sol";
import "./VoteTiming.sol";
import "./VotingToken.sol";
import "./Constants.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
/**
* @title Voting system for Oracle.
* @dev Handles receiving and resolving price requests via a commit-reveal voting scheme.
*/
contract Voting is
Testable,
Ownable,
OracleInterface,
OracleAncillaryInterface, // Interface to support ancillary data with price requests.
VotingInterface,
VotingAncillaryInterface // Interface to support ancillary data with voting rounds.
{
using FixedPoint for FixedPoint.Unsigned;
using SafeMath for uint256;
using VoteTiming for VoteTiming.Data;
using ResultComputation for ResultComputation.Data;
/****************************************
* VOTING DATA STRUCTURES *
****************************************/
// Identifies a unique price request for which the Oracle will always return the same value.
// Tracks ongoing votes as well as the result of the vote.
struct PriceRequest {
bytes32 identifier;
uint256 time;
// A map containing all votes for this price in various rounds.
mapping(uint256 => VoteInstance) voteInstances;
// If in the past, this was the voting round where this price was resolved. If current or the upcoming round,
// this is the voting round where this price will be voted on, but not necessarily resolved.
uint256 lastVotingRound;
// The index in the `pendingPriceRequests` that references this PriceRequest. A value of UINT_MAX means that
// this PriceRequest is resolved and has been cleaned up from `pendingPriceRequests`.
uint256 index;
bytes ancillaryData;
}
struct VoteInstance {
// Maps (voterAddress) to their submission.
mapping(address => VoteSubmission) voteSubmissions;
// The data structure containing the computed voting results.
ResultComputation.Data resultComputation;
}
struct VoteSubmission {
// A bytes32 of `0` indicates no commit or a commit that was already revealed.
bytes32 commit;
// The hash of the value that was revealed.
// Note: this is only used for computation of rewards.
bytes32 revealHash;
}
struct Round {
uint256 snapshotId; // Voting token snapshot ID for this round. 0 if no snapshot has been taken.
FixedPoint.Unsigned inflationRate; // Inflation rate set for this round.
FixedPoint.Unsigned gatPercentage; // Gat rate set for this round.
uint256 rewardsExpirationTime; // Time that rewards for this round can be claimed until.
}
// Represents the status a price request has.
enum RequestStatus {
NotRequested, // Was never requested.
Active, // Is being voted on in the current round.
Resolved, // Was resolved in a previous round.
Future // Is scheduled to be voted on in a future round.
}
// Only used as a return value in view methods -- never stored in the contract.
struct RequestState {
RequestStatus status;
uint256 lastVotingRound;
}
/****************************************
* INTERNAL TRACKING *
****************************************/
// Maps round numbers to the rounds.
mapping(uint256 => Round) public rounds;
// Maps price request IDs to the PriceRequest struct.
mapping(bytes32 => PriceRequest) private priceRequests;
// Price request ids for price requests that haven't yet been marked as resolved.
// These requests may be for future rounds.
bytes32[] internal pendingPriceRequests;
VoteTiming.Data public voteTiming;
// Percentage of the total token supply that must be used in a vote to
// create a valid price resolution. 1 == 100%.
FixedPoint.Unsigned public gatPercentage;
// Global setting for the rate of inflation per vote. This is the percentage of the snapshotted total supply that
// should be split among the correct voters.
// Note: this value is used to set per-round inflation at the beginning of each round. 1 = 100%.
FixedPoint.Unsigned public inflationRate;
// Time in seconds from the end of the round in which a price request is
// resolved that voters can still claim their rewards.
uint256 public rewardsExpirationTimeout;
// Reference to the voting token.
VotingToken public votingToken;
// Reference to the Finder.
FinderInterface private finder;
// If non-zero, this contract has been migrated to this address. All voters and
// financial contracts should query the new address only.
address public migratedAddress;
// Max value of an unsigned integer.
uint256 private constant UINT_MAX = ~uint256(0);
// Max length in bytes of ancillary data that can be appended to a price request.
// As of December 2020, the current Ethereum gas limit is 12.5 million. This requestPrice function's gas primarily
// comes from computing a Keccak-256 hash in _encodePriceRequest and writing a new PriceRequest to
// storage. We have empirically determined an ancillary data limit of 8192 bytes that keeps this function
// well within the gas limit at ~8 million gas. To learn more about the gas limit and EVM opcode costs go here:
// - https://etherscan.io/chart/gaslimit
// - https://github.com/djrtwo/evm-opcode-gas-costs
uint256 public constant ancillaryBytesLimit = 8192;
bytes32 public snapshotMessageHash = ECDSA.toEthSignedMessageHash(keccak256(bytes("Sign For Snapshot")));
/***************************************
* EVENTS *
****************************************/
event VoteCommitted(
address indexed voter,
uint256 indexed roundId,
bytes32 indexed identifier,
uint256 time,
bytes ancillaryData
);
event EncryptedVote(
address indexed voter,
uint256 indexed roundId,
bytes32 indexed identifier,
uint256 time,
bytes ancillaryData,
bytes encryptedVote
);
event VoteRevealed(
address indexed voter,
uint256 indexed roundId,
bytes32 indexed identifier,
uint256 time,
int256 price,
bytes ancillaryData,
uint256 numTokens
);
event RewardsRetrieved(
address indexed voter,
uint256 indexed roundId,
bytes32 indexed identifier,
uint256 time,
bytes ancillaryData,
uint256 numTokens
);
event PriceRequestAdded(uint256 indexed roundId, bytes32 indexed identifier, uint256 time);
event PriceResolved(
uint256 indexed roundId,
bytes32 indexed identifier,
uint256 time,
int256 price,
bytes ancillaryData
);
/**
* @notice Construct the Voting contract.
* @param _phaseLength length of the commit and reveal phases in seconds.
* @param _gatPercentage of the total token supply that must be used in a vote to create a valid price resolution.
* @param _inflationRate percentage inflation per round used to increase token supply of correct voters.
* @param _rewardsExpirationTimeout timeout, in seconds, within which rewards must be claimed.
* @param _votingToken address of the UMA token contract used to commit votes.
* @param _finder keeps track of all contracts within the system based on their interfaceName.
* @param _timerAddress Contract that stores the current time in a testing environment.
* Must be set to 0x0 for production environments that use live time.
*/
constructor(
uint256 _phaseLength,
FixedPoint.Unsigned memory _gatPercentage,
FixedPoint.Unsigned memory _inflationRate,
uint256 _rewardsExpirationTimeout,
address _votingToken,
address _finder,
address _timerAddress
) Testable(_timerAddress) {
voteTiming.init(_phaseLength);
require(_gatPercentage.isLessThanOrEqual(1), "GAT percentage must be <= 100%");
gatPercentage = _gatPercentage;
inflationRate = _inflationRate;
votingToken = VotingToken(_votingToken);
finder = FinderInterface(_finder);
rewardsExpirationTimeout = _rewardsExpirationTimeout;
}
/***************************************
MODIFIERS
****************************************/
modifier onlyRegisteredContract() {
if (migratedAddress != address(0)) {
require(msg.sender == migratedAddress, "Caller must be migrated address");
} else {
Registry registry = Registry(finder.getImplementationAddress(OracleInterfaces.Registry));
require(registry.isContractRegistered(msg.sender), "Called must be registered");
}
_;
}
modifier onlyIfNotMigrated() {
require(migratedAddress == address(0), "Only call this if not migrated");
_;
}
/****************************************
* PRICE REQUEST AND ACCESS FUNCTIONS *
****************************************/
/**
* @notice Enqueues a request (if a request isn't already present) for the given `identifier`, `time` pair.
* @dev Time must be in the past and the identifier must be supported. The length of the ancillary data
* is limited such that this method abides by the EVM transaction gas limit.
* @param identifier uniquely identifies the price requested. eg BTC/USD (encoded as bytes32) could be requested.
* @param time unix timestamp for the price request.
* @param ancillaryData arbitrary data appended to a price request to give the voters more info from the caller.
*/
function requestPrice(
bytes32 identifier,
uint256 time,
bytes memory ancillaryData
) public override onlyRegisteredContract() {
uint256 blockTime = getCurrentTime();
require(time <= blockTime, "Can only request in past");
require(_getIdentifierWhitelist().isIdentifierSupported(identifier), "Unsupported identifier request");
require(ancillaryData.length <= ancillaryBytesLimit, "Invalid ancillary data");
bytes32 priceRequestId = _encodePriceRequest(identifier, time, ancillaryData);
PriceRequest storage priceRequest = priceRequests[priceRequestId];
uint256 currentRoundId = voteTiming.computeCurrentRoundId(blockTime);
RequestStatus requestStatus = _getRequestStatus(priceRequest, currentRoundId);
if (requestStatus == RequestStatus.NotRequested) {
// Price has never been requested.
// Price requests always go in the next round, so add 1 to the computed current round.
uint256 nextRoundId = currentRoundId.add(1);
PriceRequest storage newPriceRequest = priceRequests[priceRequestId];
newPriceRequest.identifier = identifier;
newPriceRequest.time = time;
newPriceRequest.lastVotingRound = nextRoundId;
newPriceRequest.index = pendingPriceRequests.length;
newPriceRequest.ancillaryData = ancillaryData;
pendingPriceRequests.push(priceRequestId);
emit PriceRequestAdded(nextRoundId, identifier, time);
}
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function requestPrice(bytes32 identifier, uint256 time) public override {
requestPrice(identifier, time, "");
}
/**
* @notice Whether the price for `identifier` and `time` is available.
* @dev Time must be in the past and the identifier must be supported.
* @param identifier uniquely identifies the price requested. eg BTC/USD (encoded as bytes32) could be requested.
* @param time unix timestamp of for the price request.
* @param ancillaryData arbitrary data appended to a price request to give the voters more info from the caller.
* @return _hasPrice bool if the DVM has resolved to a price for the given identifier and timestamp.
*/
function hasPrice(
bytes32 identifier,
uint256 time,
bytes memory ancillaryData
) public view override onlyRegisteredContract() returns (bool) {
(bool _hasPrice, , ) = _getPriceOrError(identifier, time, ancillaryData);
return _hasPrice;
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function hasPrice(bytes32 identifier, uint256 time) public view override returns (bool) {
return hasPrice(identifier, time, "");
}
/**
* @notice Gets the price for `identifier` and `time` if it has already been requested and resolved.
* @dev If the price is not available, the method reverts.
* @param identifier uniquely identifies the price requested. eg BTC/USD (encoded as bytes32) could be requested.
* @param time unix timestamp of for the price request.
* @param ancillaryData arbitrary data appended to a price request to give the voters more info from the caller.
* @return int256 representing the resolved price for the given identifier and timestamp.
*/
function getPrice(
bytes32 identifier,
uint256 time,
bytes memory ancillaryData
) public view override onlyRegisteredContract() returns (int256) {
(bool _hasPrice, int256 price, string memory message) = _getPriceOrError(identifier, time, ancillaryData);
// If the price wasn't available, revert with the provided message.
require(_hasPrice, message);
return price;
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function getPrice(bytes32 identifier, uint256 time) public view override returns (int256) {
return getPrice(identifier, time, "");
}
/**
* @notice Gets the status of a list of price requests, identified by their identifier and time.
* @dev If the status for a particular request is NotRequested, the lastVotingRound will always be 0.
* @param requests array of type PendingRequest which includes an identifier and timestamp for each request.
* @return requestStates a list, in the same order as the input list, giving the status of each of the specified price requests.
*/
function getPriceRequestStatuses(PendingRequestAncillary[] memory requests)
public
view
returns (RequestState[] memory)
{
RequestState[] memory requestStates = new RequestState[](requests.length);
uint256 currentRoundId = voteTiming.computeCurrentRoundId(getCurrentTime());
for (uint256 i = 0; i < requests.length; i++) {
PriceRequest storage priceRequest =
_getPriceRequest(requests[i].identifier, requests[i].time, requests[i].ancillaryData);
RequestStatus status = _getRequestStatus(priceRequest, currentRoundId);
// If it's an active request, its true lastVotingRound is the current one, even if it hasn't been updated.
if (status == RequestStatus.Active) {
requestStates[i].lastVotingRound = currentRoundId;
} else {
requestStates[i].lastVotingRound = priceRequest.lastVotingRound;
}
requestStates[i].status = status;
}
return requestStates;
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function getPriceRequestStatuses(PendingRequest[] memory requests) public view returns (RequestState[] memory) {
PendingRequestAncillary[] memory requestsAncillary = new PendingRequestAncillary[](requests.length);
for (uint256 i = 0; i < requests.length; i++) {
requestsAncillary[i].identifier = requests[i].identifier;
requestsAncillary[i].time = requests[i].time;
requestsAncillary[i].ancillaryData = "";
}
return getPriceRequestStatuses(requestsAncillary);
}
/****************************************
* VOTING FUNCTIONS *
****************************************/
/**
* @notice Commit a vote for a price request for `identifier` at `time`.
* @dev `identifier`, `time` must correspond to a price request that's currently in the commit phase.
* Commits can be changed.
* @dev Since transaction data is public, the salt will be revealed with the vote. While this is the system’s expected behavior,
* voters should never reuse salts. If someone else is able to guess the voted price and knows that a salt will be reused, then
* they can determine the vote pre-reveal.
* @param identifier uniquely identifies the committed vote. EG BTC/USD price pair.
* @param time unix timestamp of the price being voted on.
* @param ancillaryData arbitrary data appended to a price request to give the voters more info from the caller.
* @param hash keccak256 hash of the `price`, `salt`, voter `address`, `time`, current `roundId`, and `identifier`.
*/
function commitVote(
bytes32 identifier,
uint256 time,
bytes memory ancillaryData,
bytes32 hash
) public override onlyIfNotMigrated() {
require(hash != bytes32(0), "Invalid provided hash");
// Current time is required for all vote timing queries.
uint256 blockTime = getCurrentTime();
require(
voteTiming.computeCurrentPhase(blockTime) == VotingAncillaryInterface.Phase.Commit,
"Cannot commit in reveal phase"
);
// At this point, the computed and last updated round ID should be equal.
uint256 currentRoundId = voteTiming.computeCurrentRoundId(blockTime);
PriceRequest storage priceRequest = _getPriceRequest(identifier, time, ancillaryData);
require(
_getRequestStatus(priceRequest, currentRoundId) == RequestStatus.Active,
"Cannot commit inactive request"
);
priceRequest.lastVotingRound = currentRoundId;
VoteInstance storage voteInstance = priceRequest.voteInstances[currentRoundId];
voteInstance.voteSubmissions[msg.sender].commit = hash;
emit VoteCommitted(msg.sender, currentRoundId, identifier, time, ancillaryData);
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function commitVote(
bytes32 identifier,
uint256 time,
bytes32 hash
) public override onlyIfNotMigrated() {
commitVote(identifier, time, "", hash);
}
/**
* @notice Snapshot the current round's token balances and lock in the inflation rate and GAT.
* @dev This function can be called multiple times, but only the first call per round into this function or `revealVote`
* will create the round snapshot. Any later calls will be a no-op. Will revert unless called during reveal period.
* @param signature signature required to prove caller is an EOA to prevent flash loans from being included in the
* snapshot.
*/
function snapshotCurrentRound(bytes calldata signature)
external
override(VotingInterface, VotingAncillaryInterface)
onlyIfNotMigrated()
{
uint256 blockTime = getCurrentTime();
require(voteTiming.computeCurrentPhase(blockTime) == Phase.Reveal, "Only snapshot in reveal phase");
// Require public snapshot require signature to ensure caller is an EOA.
require(ECDSA.recover(snapshotMessageHash, signature) == msg.sender, "Signature must match sender");
uint256 roundId = voteTiming.computeCurrentRoundId(blockTime);
_freezeRoundVariables(roundId);
}
/**
* @notice Reveal a previously committed vote for `identifier` at `time`.
* @dev The revealed `price`, `salt`, `address`, `time`, `roundId`, and `identifier`, must hash to the latest `hash`
* that `commitVote()` was called with. Only the committer can reveal their vote.
* @param identifier voted on in the commit phase. EG BTC/USD price pair.
* @param time specifies the unix timestamp of the price being voted on.
* @param price voted on during the commit phase.
* @param ancillaryData arbitrary data appended to a price request to give the voters more info from the caller.
* @param salt value used to hide the commitment price during the commit phase.
*/
function revealVote(
bytes32 identifier,
uint256 time,
int256 price,
bytes memory ancillaryData,
int256 salt
) public override onlyIfNotMigrated() {
require(voteTiming.computeCurrentPhase(getCurrentTime()) == Phase.Reveal, "Cannot reveal in commit phase");
// Note: computing the current round is required to disallow people from revealing an old commit after the round is over.
uint256 roundId = voteTiming.computeCurrentRoundId(getCurrentTime());
PriceRequest storage priceRequest = _getPriceRequest(identifier, time, ancillaryData);
VoteInstance storage voteInstance = priceRequest.voteInstances[roundId];
VoteSubmission storage voteSubmission = voteInstance.voteSubmissions[msg.sender];
// Scoping to get rid of a stack too deep error.
{
// 0 hashes are disallowed in the commit phase, so they indicate a different error.
// Cannot reveal an uncommitted or previously revealed hash
require(voteSubmission.commit != bytes32(0), "Invalid hash reveal");
require(
keccak256(abi.encodePacked(price, salt, msg.sender, time, ancillaryData, roundId, identifier)) ==
voteSubmission.commit,
"Revealed data != commit hash"
);
// To protect against flash loans, we require snapshot be validated as EOA.
require(rounds[roundId].snapshotId != 0, "Round has no snapshot");
}
// Get the frozen snapshotId
uint256 snapshotId = rounds[roundId].snapshotId;
delete voteSubmission.commit;
// Get the voter's snapshotted balance. Since balances are returned pre-scaled by 10**18, we can directly
// initialize the Unsigned value with the returned uint.
FixedPoint.Unsigned memory balance = FixedPoint.Unsigned(votingToken.balanceOfAt(msg.sender, snapshotId));
// Set the voter's submission.
voteSubmission.revealHash = keccak256(abi.encode(price));
// Add vote to the results.
voteInstance.resultComputation.addVote(price, balance);
emit VoteRevealed(msg.sender, roundId, identifier, time, price, ancillaryData, balance.rawValue);
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function revealVote(
bytes32 identifier,
uint256 time,
int256 price,
int256 salt
) public override {
revealVote(identifier, time, price, "", salt);
}
/**
* @notice commits a vote and logs an event with a data blob, typically an encrypted version of the vote
* @dev An encrypted version of the vote is emitted in an event `EncryptedVote` to allow off-chain infrastructure to
* retrieve the commit. The contents of `encryptedVote` are never used on chain: it is purely for convenience.
* @param identifier unique price pair identifier. Eg: BTC/USD price pair.
* @param time unix timestamp of for the price request.
* @param ancillaryData arbitrary data appended to a price request to give the voters more info from the caller.
* @param hash keccak256 hash of the price you want to vote for and a `int256 salt`.
* @param encryptedVote offchain encrypted blob containing the voters amount, time and salt.
*/
function commitAndEmitEncryptedVote(
bytes32 identifier,
uint256 time,
bytes memory ancillaryData,
bytes32 hash,
bytes memory encryptedVote
) public override {
commitVote(identifier, time, ancillaryData, hash);
uint256 roundId = voteTiming.computeCurrentRoundId(getCurrentTime());
emit EncryptedVote(msg.sender, roundId, identifier, time, ancillaryData, encryptedVote);
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function commitAndEmitEncryptedVote(
bytes32 identifier,
uint256 time,
bytes32 hash,
bytes memory encryptedVote
) public override {
commitVote(identifier, time, "", hash);
commitAndEmitEncryptedVote(identifier, time, "", hash, encryptedVote);
}
/**
* @notice Submit a batch of commits in a single transaction.
* @dev Using `encryptedVote` is optional. If included then commitment is emitted in an event.
* Look at `project-root/common/Constants.js` for the tested maximum number of
* commitments that can fit in one transaction.
* @param commits struct to encapsulate an `identifier`, `time`, `hash` and optional `encryptedVote`.
*/
function batchCommit(CommitmentAncillary[] memory commits) public override {
for (uint256 i = 0; i < commits.length; i++) {
if (commits[i].encryptedVote.length == 0) {
commitVote(commits[i].identifier, commits[i].time, commits[i].ancillaryData, commits[i].hash);
} else {
commitAndEmitEncryptedVote(
commits[i].identifier,
commits[i].time,
commits[i].ancillaryData,
commits[i].hash,
commits[i].encryptedVote
);
}
}
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function batchCommit(Commitment[] memory commits) public override {
CommitmentAncillary[] memory commitsAncillary = new CommitmentAncillary[](commits.length);
for (uint256 i = 0; i < commits.length; i++) {
commitsAncillary[i].identifier = commits[i].identifier;
commitsAncillary[i].time = commits[i].time;
commitsAncillary[i].ancillaryData = "";
commitsAncillary[i].hash = commits[i].hash;
commitsAncillary[i].encryptedVote = commits[i].encryptedVote;
}
batchCommit(commitsAncillary);
}
/**
* @notice Reveal multiple votes in a single transaction.
* Look at `project-root/common/Constants.js` for the tested maximum number of reveals.
* that can fit in one transaction.
* @dev For more info on reveals, review the comment for `revealVote`.
* @param reveals array of the Reveal struct which contains an identifier, time, price and salt.
*/
function batchReveal(RevealAncillary[] memory reveals) public override {
for (uint256 i = 0; i < reveals.length; i++) {
revealVote(
reveals[i].identifier,
reveals[i].time,
reveals[i].price,
reveals[i].ancillaryData,
reveals[i].salt
);
}
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function batchReveal(Reveal[] memory reveals) public override {
RevealAncillary[] memory revealsAncillary = new RevealAncillary[](reveals.length);
for (uint256 i = 0; i < reveals.length; i++) {
revealsAncillary[i].identifier = reveals[i].identifier;
revealsAncillary[i].time = reveals[i].time;
revealsAncillary[i].price = reveals[i].price;
revealsAncillary[i].ancillaryData = "";
revealsAncillary[i].salt = reveals[i].salt;
}
batchReveal(revealsAncillary);
}
/**
* @notice Retrieves rewards owed for a set of resolved price requests.
* @dev Can only retrieve rewards if calling for a valid round and if the call is done within the timeout threshold
* (not expired). Note that a named return value is used here to avoid a stack to deep error.
* @param voterAddress voter for which rewards will be retrieved. Does not have to be the caller.
* @param roundId the round from which voting rewards will be retrieved from.
* @param toRetrieve array of PendingRequests which rewards are retrieved from.
* @return totalRewardToIssue total amount of rewards returned to the voter.
*/
function retrieveRewards(
address voterAddress,
uint256 roundId,
PendingRequestAncillary[] memory toRetrieve
) public override returns (FixedPoint.Unsigned memory totalRewardToIssue) {
if (migratedAddress != address(0)) {
require(msg.sender == migratedAddress, "Can only call from migrated");
}
require(roundId < voteTiming.computeCurrentRoundId(getCurrentTime()), "Invalid roundId");
Round storage round = rounds[roundId];
bool isExpired = getCurrentTime() > round.rewardsExpirationTime;
FixedPoint.Unsigned memory snapshotBalance =
FixedPoint.Unsigned(votingToken.balanceOfAt(voterAddress, round.snapshotId));
// Compute the total amount of reward that will be issued for each of the votes in the round.
FixedPoint.Unsigned memory snapshotTotalSupply =
FixedPoint.Unsigned(votingToken.totalSupplyAt(round.snapshotId));
FixedPoint.Unsigned memory totalRewardPerVote = round.inflationRate.mul(snapshotTotalSupply);
// Keep track of the voter's accumulated token reward.
totalRewardToIssue = FixedPoint.Unsigned(0);
for (uint256 i = 0; i < toRetrieve.length; i++) {
PriceRequest storage priceRequest =
_getPriceRequest(toRetrieve[i].identifier, toRetrieve[i].time, toRetrieve[i].ancillaryData);
VoteInstance storage voteInstance = priceRequest.voteInstances[priceRequest.lastVotingRound];
// Only retrieve rewards for votes resolved in same round
require(priceRequest.lastVotingRound == roundId, "Retrieve for votes same round");
_resolvePriceRequest(priceRequest, voteInstance);
if (voteInstance.voteSubmissions[voterAddress].revealHash == 0) {
continue;
} else if (isExpired) {
// Emit a 0 token retrieval on expired rewards.
emit RewardsRetrieved(
voterAddress,
roundId,
toRetrieve[i].identifier,
toRetrieve[i].time,
toRetrieve[i].ancillaryData,
0
);
} else if (
voteInstance.resultComputation.wasVoteCorrect(voteInstance.voteSubmissions[voterAddress].revealHash)
) {
// The price was successfully resolved during the voter's last voting round, the voter revealed
// and was correct, so they are eligible for a reward.
// Compute the reward and add to the cumulative reward.
FixedPoint.Unsigned memory reward =
snapshotBalance.mul(totalRewardPerVote).div(
voteInstance.resultComputation.getTotalCorrectlyVotedTokens()
);
totalRewardToIssue = totalRewardToIssue.add(reward);
// Emit reward retrieval for this vote.
emit RewardsRetrieved(
voterAddress,
roundId,
toRetrieve[i].identifier,
toRetrieve[i].time,
toRetrieve[i].ancillaryData,
reward.rawValue
);
} else {
// Emit a 0 token retrieval on incorrect votes.
emit RewardsRetrieved(
voterAddress,
roundId,
toRetrieve[i].identifier,
toRetrieve[i].time,
toRetrieve[i].ancillaryData,
0
);
}
// Delete the submission to capture any refund and clean up storage.
delete voteInstance.voteSubmissions[voterAddress].revealHash;
}
// Issue any accumulated rewards.
if (totalRewardToIssue.isGreaterThan(0)) {
require(votingToken.mint(voterAddress, totalRewardToIssue.rawValue), "Voting token issuance failed");
}
}
// Overloaded method to enable short term backwards compatibility. Will be deprecated in the next DVM version.
function retrieveRewards(
address voterAddress,
uint256 roundId,
PendingRequest[] memory toRetrieve
) public override returns (FixedPoint.Unsigned memory) {
PendingRequestAncillary[] memory toRetrieveAncillary = new PendingRequestAncillary[](toRetrieve.length);
for (uint256 i = 0; i < toRetrieve.length; i++) {
toRetrieveAncillary[i].identifier = toRetrieve[i].identifier;
toRetrieveAncillary[i].time = toRetrieve[i].time;
toRetrieveAncillary[i].ancillaryData = "";
}
return retrieveRewards(voterAddress, roundId, toRetrieveAncillary);
}
/****************************************
* VOTING GETTER FUNCTIONS *
****************************************/
/**
* @notice Gets the queries that are being voted on this round.
* @return pendingRequests array containing identifiers of type `PendingRequest`.
* and timestamps for all pending requests.
*/
function getPendingRequests()
external
view
override(VotingInterface, VotingAncillaryInterface)
returns (PendingRequestAncillary[] memory)
{
uint256 blockTime = getCurrentTime();
uint256 currentRoundId = voteTiming.computeCurrentRoundId(blockTime);
// Solidity memory arrays aren't resizable (and reading storage is expensive). Hence this hackery to filter
// `pendingPriceRequests` only to those requests that have an Active RequestStatus.
PendingRequestAncillary[] memory unresolved = new PendingRequestAncillary[](pendingPriceRequests.length);
uint256 numUnresolved = 0;
for (uint256 i = 0; i < pendingPriceRequests.length; i++) {
PriceRequest storage priceRequest = priceRequests[pendingPriceRequests[i]];
if (_getRequestStatus(priceRequest, currentRoundId) == RequestStatus.Active) {
unresolved[numUnresolved] = PendingRequestAncillary({
identifier: priceRequest.identifier,
time: priceRequest.time,
ancillaryData: priceRequest.ancillaryData
});
numUnresolved++;
}
}
PendingRequestAncillary[] memory pendingRequests = new PendingRequestAncillary[](numUnresolved);
for (uint256 i = 0; i < numUnresolved; i++) {
pendingRequests[i] = unresolved[i];
}
return pendingRequests;
}
/**
* @notice Returns the current voting phase, as a function of the current time.
* @return Phase to indicate the current phase. Either { Commit, Reveal, NUM_PHASES_PLACEHOLDER }.
*/
function getVotePhase() external view override(VotingInterface, VotingAncillaryInterface) returns (Phase) {
return voteTiming.computeCurrentPhase(getCurrentTime());
}
/**
* @notice Returns the current round ID, as a function of the current time.
* @return uint256 representing the unique round ID.
*/
function getCurrentRoundId() external view override(VotingInterface, VotingAncillaryInterface) returns (uint256) {
return voteTiming.computeCurrentRoundId(getCurrentTime());
}
/****************************************
* OWNER ADMIN FUNCTIONS *
****************************************/
/**
* @notice Disables this Voting contract in favor of the migrated one.
* @dev Can only be called by the contract owner.
* @param newVotingAddress the newly migrated contract address.
*/
function setMigrated(address newVotingAddress)
external
override(VotingInterface, VotingAncillaryInterface)
onlyOwner
{
migratedAddress = newVotingAddress;
}
/**
* @notice Resets the inflation rate. Note: this change only applies to rounds that have not yet begun.
* @dev This method is public because calldata structs are not currently supported by solidity.
* @param newInflationRate sets the next round's inflation rate.
*/
function setInflationRate(FixedPoint.Unsigned memory newInflationRate)
public
override(VotingInterface, VotingAncillaryInterface)
onlyOwner
{
inflationRate = newInflationRate;
}
/**
* @notice Resets the Gat percentage. Note: this change only applies to rounds that have not yet begun.
* @dev This method is public because calldata structs are not currently supported by solidity.
* @param newGatPercentage sets the next round's Gat percentage.
*/
function setGatPercentage(FixedPoint.Unsigned memory newGatPercentage)
public
override(VotingInterface, VotingAncillaryInterface)
onlyOwner
{
require(newGatPercentage.isLessThan(1), "GAT percentage must be < 100%");
gatPercentage = newGatPercentage;
}
/**
* @notice Resets the rewards expiration timeout.
* @dev This change only applies to rounds that have not yet begun.
* @param NewRewardsExpirationTimeout how long a caller can wait before choosing to withdraw their rewards.
*/
function setRewardsExpirationTimeout(uint256 NewRewardsExpirationTimeout)
public
override(VotingInterface, VotingAncillaryInterface)
onlyOwner
{
rewardsExpirationTimeout = NewRewardsExpirationTimeout;
}
/****************************************
* PRIVATE AND INTERNAL FUNCTIONS *
****************************************/
// Returns the price for a given identifer. Three params are returns: bool if there was an error, int to represent
// the resolved price and a string which is filled with an error message, if there was an error or "".
function _getPriceOrError(
bytes32 identifier,
uint256 time,
bytes memory ancillaryData
)
private
view
returns (
bool,
int256,
string memory
)
{
PriceRequest storage priceRequest = _getPriceRequest(identifier, time, ancillaryData);
uint256 currentRoundId = voteTiming.computeCurrentRoundId(getCurrentTime());
RequestStatus requestStatus = _getRequestStatus(priceRequest, currentRoundId);
if (requestStatus == RequestStatus.Active) {
return (false, 0, "Current voting round not ended");
} else if (requestStatus == RequestStatus.Resolved) {
VoteInstance storage voteInstance = priceRequest.voteInstances[priceRequest.lastVotingRound];
(, int256 resolvedPrice) =
voteInstance.resultComputation.getResolvedPrice(_computeGat(priceRequest.lastVotingRound));
return (true, resolvedPrice, "");
} else if (requestStatus == RequestStatus.Future) {
return (false, 0, "Price is still to be voted on");
} else {
return (false, 0, "Price was never requested");
}
}
function _getPriceRequest(
bytes32 identifier,
uint256 time,
bytes memory ancillaryData
) private view returns (PriceRequest storage) {
return priceRequests[_encodePriceRequest(identifier, time, ancillaryData)];
}
function _encodePriceRequest(
bytes32 identifier,
uint256 time,
bytes memory ancillaryData
) private pure returns (bytes32) {
return keccak256(abi.encode(identifier, time, ancillaryData));
}
function _freezeRoundVariables(uint256 roundId) private {
Round storage round = rounds[roundId];
// Only on the first reveal should the snapshot be captured for that round.
if (round.snapshotId == 0) {
// There is no snapshot ID set, so create one.
round.snapshotId = votingToken.snapshot();
// Set the round inflation rate to the current global inflation rate.
rounds[roundId].inflationRate = inflationRate;
// Set the round gat percentage to the current global gat rate.
rounds[roundId].gatPercentage = gatPercentage;
// Set the rewards expiration time based on end of time of this round and the current global timeout.
rounds[roundId].rewardsExpirationTime = voteTiming.computeRoundEndTime(roundId).add(
rewardsExpirationTimeout
);
}
}
function _resolvePriceRequest(PriceRequest storage priceRequest, VoteInstance storage voteInstance) private {
if (priceRequest.index == UINT_MAX) {
return;
}
(bool isResolved, int256 resolvedPrice) =
voteInstance.resultComputation.getResolvedPrice(_computeGat(priceRequest.lastVotingRound));
require(isResolved, "Can't resolve unresolved request");
// Delete the resolved price request from pendingPriceRequests.
uint256 lastIndex = pendingPriceRequests.length - 1;
PriceRequest storage lastPriceRequest = priceRequests[pendingPriceRequests[lastIndex]];
lastPriceRequest.index = priceRequest.index;
pendingPriceRequests[priceRequest.index] = pendingPriceRequests[lastIndex];
pendingPriceRequests.pop();
priceRequest.index = UINT_MAX;
emit PriceResolved(
priceRequest.lastVotingRound,
priceRequest.identifier,
priceRequest.time,
resolvedPrice,
priceRequest.ancillaryData
);
}
function _computeGat(uint256 roundId) private view returns (FixedPoint.Unsigned memory) {
uint256 snapshotId = rounds[roundId].snapshotId;
if (snapshotId == 0) {
// No snapshot - return max value to err on the side of caution.
return FixedPoint.Unsigned(UINT_MAX);
}
// Grab the snapshotted supply from the voting token. It's already scaled by 10**18, so we can directly
// initialize the Unsigned value with the returned uint.
FixedPoint.Unsigned memory snapshottedSupply = FixedPoint.Unsigned(votingToken.totalSupplyAt(snapshotId));
// Multiply the total supply at the snapshot by the gatPercentage to get the GAT in number of tokens.
return snapshottedSupply.mul(rounds[roundId].gatPercentage);
}
function _getRequestStatus(PriceRequest storage priceRequest, uint256 currentRoundId)
private
view
returns (RequestStatus)
{
if (priceRequest.lastVotingRound == 0) {
return RequestStatus.NotRequested;
} else if (priceRequest.lastVotingRound < currentRoundId) {
VoteInstance storage voteInstance = priceRequest.voteInstances[priceRequest.lastVotingRound];
(bool isResolved, ) =
voteInstance.resultComputation.getResolvedPrice(_computeGat(priceRequest.lastVotingRound));
return isResolved ? RequestStatus.Resolved : RequestStatus.Active;
} else if (priceRequest.lastVotingRound == currentRoundId) {
return RequestStatus.Active;
} else {
// Means than priceRequest.lastVotingRound > currentRoundId
return RequestStatus.Future;
}
}
function _getIdentifierWhitelist() private view returns (IdentifierWhitelistInterface supportedIdentifiers) {
return IdentifierWhitelistInterface(finder.getImplementationAddress(OracleInterfaces.IdentifierWhitelist));
}
}