Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Oracle contract #16

Merged
merged 7 commits into from
Sep 23, 2020
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 40 additions & 46 deletions apps/depooloracle/contracts/Algorithm.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,59 +7,53 @@ library Algorithm {
using SafeMath for uint256;

/**
* Computes a median of a non-empty array, modifying it in process (!)
* Computes mode of a non-empty array, if array is unimodal.
* Low gas cost.
*/
function modifyingMedian(uint256[] data) internal returns (uint256) {
// TODO quickselect with Hoare partition scheme
function mode(uint256[] data) internal pure returns (bool, uint256) {

assert(0 != data.length);
sort(data);

if (data.length % 2 == 1) {
return data[data.length.div(2)];
} else {
return data[data.length.div(2)].add(data[data.length.div(2).sub(1)]).div(2);
}
}

/**
* Sorts an array in-place
*/
function sort(uint256[] data) internal {
// Based on https://ethereum.stackexchange.com/a/1518
if (0 == data.length)
return;

_quickSort(data, 0, data.length.sub(1));
}

function _quickSort(uint256[] memory arr, uint256 left, uint256 right) internal {
// Based on https://ethereum.stackexchange.com/a/1518
uint256 i = left;
uint256 j = right;
if (i == j)
return;

uint256 pivot = arr[left.add(right.sub(left).div(2))];
while (i <= j) {
while (arr[i] < pivot)
i = i.add(1);

while (arr[j] > pivot)
j = j.sub(1);

if (i <= j) {
(arr[i], arr[j]) = (arr[j], arr[i]);
i = i.add(1);
if (0 == j)
// allocate arrays
uint256[] memory dataValues = new uint256[](data.length);
uint256[] memory dataValuesCounts = new uint256[](data.length);

// initialize first element
dataValues[0] = data[0];
dataValuesCounts[0] = 1;
uint256 dataValuesLength = 1;

// process data
uint256 i = 0;
uint256 j = 0;
bool complete;
for (i = 1; i < data.length; i++) {
complete = true;
for (j = 0; j < dataValuesLength; j++) {
lxzrv marked this conversation as resolved.
Show resolved Hide resolved
if (data[i] == dataValues[j]) {
dataValuesCounts[j]++;
complete = false;
break;
j = j.sub(1);
}
}
if (complete) {
dataValues[dataValuesLength] = data[i];
dataValuesCounts[dataValuesLength]++;
dataValuesLength++;
}
}

if (left < j)
_quickSort(arr, left, j);
if (i < right)
_quickSort(arr, i, right);
// find mode value index
uint256 mostFrequentValueIndex = 0;
for (i = 1; i < dataValuesLength; i++)
if (dataValuesCounts[i] > dataValuesCounts[mostFrequentValueIndex])
mostFrequentValueIndex = i;

// check if data is unimodal
for (i = 0; i < dataValuesLength; i++)
if ((i != mostFrequentValueIndex) && (dataValuesCounts[i] == dataValuesCounts[mostFrequentValueIndex]))
return (false, 0);

return (true, dataValues[mostFrequentValueIndex]);
}
}
92 changes: 50 additions & 42 deletions apps/depooloracle/contracts/DePoolOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import "./BitOps.sol";
* by the DAO on the ETH 2.0 side. The balances can go up because of reward accumulation
* and can go down because of slashing.
*
* The timeline is divided into consecutive epochs. At most one data point is produced per epoch.
* The timeline is divided into consecutive reportIntervals. At most one data point is produced per reportInterval.
* A data point is considered finalized (produced) as soon as `quorum` oracle committee members
* send data.
* There can be gaps in data points if for some point `quorum` is not reached.
Expand All @@ -36,7 +36,7 @@ contract DePoolOracle is IDePoolOracle, IsContract, AragonApp {
/// @dev Maximum number of oracle committee members
uint256 public constant MAX_MEMBERS = 256;

uint256 internal constant EPOCH_DURATION = 1 days;
uint256 internal constant REPORT_INTERVAL_DURATION = 1 days;
uint256 internal constant MEMBER_NOT_FOUND = uint256(-1);

/// @dev oracle committee members
Expand All @@ -48,11 +48,11 @@ contract DePoolOracle is IDePoolOracle, IsContract, AragonApp {
IDePool public pool;

// data describing last finalized data point
uint256 private lastFinalizedEpoch;
uint256 private lastFinalizedReportInterval;
uint256 private lastFinalizedData;

// data of the current aggregation
uint256 private currentlyAggregatedEpoch;
uint256 private currentlyAggregatedReportInterval;
uint256 private contributionBitMask;
uint256[] private currentlyAggregatedData; // only indexes set in contributionBitMask are valid

Expand Down Expand Up @@ -127,30 +127,31 @@ contract DePoolOracle is IDePoolOracle, IsContract, AragonApp {
quorum = _quorum;
emit QuorumChanged(_quorum);

if (currentlyAggregatedEpoch == _getCurrentEpoch())
_tryFinalize();
if (currentlyAggregatedReportInterval <= _getCurrentReportInterval() &&
lxzrv marked this conversation as resolved.
Show resolved Hide resolved
currentlyAggregatedReportInterval > lastFinalizedReportInterval)
_tryFinalize(currentlyAggregatedReportInterval);

_assertInvariants();
}


/**
* @notice An oracle committee member pushes data from the ETH 2.0 side
* @param _epoch Epoch id
* @param _reportInterval ReportInterval id
* @param _eth2balance Balance in wei on the ETH 2.0 side
*/
function pushData(uint256 _epoch, uint256 _eth2balance) external {
require(_epoch == _getCurrentEpoch(), "EPOCH_IS_NOT_CURRENT");
assert(lastFinalizedEpoch <= _epoch);
require(lastFinalizedEpoch != _epoch, "ALREADY_FINALIZED");
function pushData(uint256 _reportInterval, uint256 _eth2balance) external {
require(_reportInterval <= _getCurrentReportInterval(), "REPORT_INTERVAL_HAS_NOT_YET_BEGUN");
require(_reportInterval >= currentlyAggregatedReportInterval, "REPORT_INTERVAL_IS_TOO_OLD");
require(_reportInterval > lastFinalizedReportInterval, "REPORT_INTERVAL_IS_TOO_OLD");

address member = msg.sender;
uint256 index = _findMember(member);
require(index != MEMBER_NOT_FOUND, "MEMBER_NOT_FOUND");

if (currentlyAggregatedEpoch != _epoch) {
// reset aggregation on new epoch
currentlyAggregatedEpoch = _epoch;
if (currentlyAggregatedReportInterval != _reportInterval) {
// reset aggregation on new reportInterval
currentlyAggregatedReportInterval = _reportInterval;
lxzrv marked this conversation as resolved.
Show resolved Hide resolved
contributionBitMask = 0;
// We don't need to waste gas resetting currentlyAggregatedData since
// we cleared the index - contributionBitMask.
Expand All @@ -164,7 +165,7 @@ contract DePoolOracle is IDePoolOracle, IsContract, AragonApp {

currentlyAggregatedData[index] = _eth2balance;

_tryFinalize();
_tryFinalize(_reportInterval);
}


Expand All @@ -184,45 +185,45 @@ contract DePoolOracle is IDePoolOracle, IsContract, AragonApp {


/**
* @notice Returns epoch duration in seconds
* @dev Epochs are consecutive time intervals. Oracle data is aggregated
* and processed for each epoch independently.
* @notice Returns reportInterval duration in seconds
* @dev ReportIntervals are consecutive time intervals. Oracle data is aggregated
* and processed for each reportInterval independently.
*/
function getEpochDurationSeconds() external view returns (uint256) {
return EPOCH_DURATION;
function getReportIntervalDurationSeconds() external view returns (uint256) {
return REPORT_INTERVAL_DURATION;
}

/**
* @notice Returns epoch id for a timestamp
* @notice Returns reportInterval id for a timestamp
* @param _timestamp Unix timestamp, seconds
*/
function getEpochForTimestamp(uint256 _timestamp) external view returns (uint256) {
return _getEpochForTimestamp(_timestamp);
function getReportIntervalForTimestamp(uint256 _timestamp) external view returns (uint256) {
return _getReportIntervalForTimestamp(_timestamp);
}

/**
* @notice Returns current epoch id
* @notice Returns current reportInterval id
*/
function getCurrentEpoch() external view returns (uint256) {
return _getCurrentEpoch();
function getCurrentReportInterval() external view returns (uint256) {
return _getCurrentReportInterval();
}


/**
* @notice Returns the latest data from the ETH 2.0 side
* @dev Depending on the oracle member committee liveness, the data can be stale. See _epoch.
* @return _epoch Epoch id
* @dev Depending on the oracle member committee liveness, the data can be stale. See _reportInterval.
* @return _reportInterval ReportInterval id
* @return _eth2balance Balance in wei on the ETH 2.0 side
*/
function getLatestData() external view returns (uint256 epoch, uint256 eth2balance) {
return (lastFinalizedEpoch, lastFinalizedData);
function getLatestData() external view returns (uint256 reportInterval, uint256 eth2balance) {
return (lastFinalizedReportInterval, lastFinalizedData);
}


/**
* @dev Finalizes the current data point if quorum is reached
*/
function _tryFinalize() internal {
function _tryFinalize(uint256 _reportInterval) internal {
uint256 mask = contributionBitMask;
uint256 popcnt = mask.popcnt();
if (popcnt < quorum)
Expand All @@ -242,14 +243,21 @@ contract DePoolOracle is IDePoolOracle, IsContract, AragonApp {
}
assert(i == data.length);

// computing a median on data
lastFinalizedData = Algorithm.modifyingMedian(data);
lastFinalizedEpoch = _getCurrentEpoch();
bool isUnimodal;
uint256 mode;

emit AggregatedData(lastFinalizedEpoch, lastFinalizedData);
(isUnimodal, mode) = Algorithm.mode(data);
if (!isUnimodal)
return;

// finalizing and reporting mode value to depool
lastFinalizedData = mode;
lastFinalizedReportInterval = _reportInterval;

emit AggregatedData(lastFinalizedReportInterval, lastFinalizedData);

if (address(0) != address(pool))
pool.reportEther2(lastFinalizedEpoch, lastFinalizedData);
pool.reportEther2(lastFinalizedReportInterval, lastFinalizedData);
}

/**
Expand Down Expand Up @@ -283,17 +291,17 @@ contract DePoolOracle is IDePoolOracle, IsContract, AragonApp {
}

/**
* @dev Returns current epoch id
* @dev Returns current reportInterval id
*/
function _getCurrentEpoch() internal view returns (uint256) {
return _getEpochForTimestamp(_getTime());
function _getCurrentReportInterval() internal view returns (uint256) {
return _getReportIntervalForTimestamp(_getTime());
}

/**
* @dev Returns epoch id for a timestamp
* @dev Returns reportInterval id for a timestamp
* @param _timestamp Unix timestamp, seconds
*/
function _getEpochForTimestamp(uint256 _timestamp) internal pure returns (uint256) {
return _timestamp.div(EPOCH_DURATION);
function _getReportIntervalForTimestamp(uint256 _timestamp) internal pure returns (uint256) {
return _timestamp.div(REPORT_INTERVAL_DURATION);
}
}
9 changes: 9 additions & 0 deletions apps/depooloracle/contracts/test_helpers/TestAlgorithm.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pragma solidity 0.4.24;

import "../Algorithm.sol";

contract TestAlgorithm {
function modeTest(uint256[] data) public pure returns (bool isUnimodal, uint256 mode) {
return Algorithm.mode(data);
}
}
Loading