Files
2022-11-25 15:14:43 +02:00

998 lines
44 KiB
Solidity
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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));
}
}