Skip to content

Commit

Permalink
fix: source round (#7)
Browse files Browse the repository at this point in the history
* fix: source round detection

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* feat: implement round updates in mean adapter

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* feat: add delayed source data

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* feat: add todos

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* feat: chainlink round traversal

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* fix: remove latest round

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* fix: remove latest round from mock

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* fix: internal function naming

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* fix: return round data

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* fix: comments

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* fix: return latest in source if no historical data

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* fix: add comment on updated at

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* feat: move all permission window logic to oev

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

* feat: pass max traversal from oev

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>

---------

Signed-off-by: Reinis Martinsons <reinis@umaproject.org>
  • Loading branch information
Reinis-FRP authored Sep 11, 2023
1 parent fb6aaed commit ff96808
Show file tree
Hide file tree
Showing 13 changed files with 224 additions and 41 deletions.
36 changes: 28 additions & 8 deletions src/OevOracle.sol
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "openzeppelin-contracts/contracts/access/Ownable.sol";
import "./interfaces/IBaseOracleAdapter.sol";
import "./interfaces/IOevOracle.sol";

abstract contract OevOracle is IOevOracle {
abstract contract OevOracle is IOevOracle, Ownable {
IBaseOracleAdapter public sourceAdapter;

uint256 public permissionWindow = 10 minutes;

// TODO: Test gas costs of traversing history to decide on reasonable value.
uint256 public maxTraversal = 10; // Max number of historical source updates to traverse.

int256 public cachedLatestAnswer; // Always 18 decimals.
uint256 public cachedLatestTimestamp;
uint256 public lastUpdateTimestamp;
Expand All @@ -28,20 +34,34 @@ abstract contract OevOracle is IOevOracle {
// TODO: Decide how to handle negative values. Since not all source oracles support negative values we may need to
// limit the support only to positive values.
function internalLatestAnswer() public view override returns (int256) {
if (canReturnCache(cachedLatestTimestamp, lastUpdateTimestamp)) return cachedLatestAnswer;
return sourceAdapter.latestAnswer();
if (canReturnCache(lastUpdateTimestamp)) return cachedLatestAnswer;
// Attempt to return latest answer as of before permission window. If this is not available return latest answer.
return sourceAdapter.tryLatestAnswerAt(block.timestamp - permissionWindow, maxTraversal);
}

function internalLatestTimestamp() public view override returns (uint256) {
if (canReturnCache(cachedLatestTimestamp, lastUpdateTimestamp)) return cachedLatestTimestamp;
return sourceAdapter.latestTimestamp();
if (canReturnCache(lastUpdateTimestamp)) return cachedLatestTimestamp;
// Attempt to return latest timestamp as of before permission window. If this is not available return latest timestamp.
return sourceAdapter.tryLatestTimestampAt(block.timestamp - permissionWindow, maxTraversal);
}

function canUpdate(address caller, uint256 cachedLatestTimestamp) public view virtual returns (bool);

function canReturnCache(uint256 _cachedLatestTimestamp, uint256 _updateTimestamp)
function setPermissionWindow(uint256 _permissionWindow) public onlyOwner {
permissionWindow = _permissionWindow;
}

function setMaxTraversal(uint256 _maxTraversal) public onlyOwner {
maxTraversal = _maxTraversal;
}

function canReturnCache(uint256 updateTimestamp)
public
view
virtual
returns (bool);
override
returns (bool)
{
// If cache was updated within the permission window, return the cached value.
return (block.timestamp - updateTimestamp < permissionWindow);
}
}
2 changes: 1 addition & 1 deletion src/adapters/BaseDestinationOracleAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ pragma solidity 0.8.17;
import "../controllers/BaseController.sol";
import "../interfaces/IBaseOracleAdapter.sol";

contract BaseDestinationOracleAdapter is BaseController, IBaseOracleAdapter {
abstract contract BaseDestinationOracleAdapter is BaseController, IBaseOracleAdapter {
constructor(address _sourceAdapter) BaseController(_sourceAdapter) {}

function decimals() public view override returns (uint8) {
Expand Down
12 changes: 12 additions & 0 deletions src/adapters/MeanSourceOracleAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,16 @@ contract MeanSourceOracleAdapter is IBaseOracleAdapter {
function decimals() public view override returns (uint8) {
return 18;
}

function tryLatestAnswerAt(uint256 timestamp, uint256 maxTraversal) public view override returns (int256) {
// TODO: implement traversing source rounds to get latest data as of requested timestamp.
// TODO: this also requires adding update method to store historical data on source adapter.
return latestAnswer();
}

function tryLatestTimestampAt(uint256 timestamp, uint256 maxTraversal) public view override returns (uint256) {
// TODO: implement traversing source rounds to get latest data as of requested timestamp.
// TODO: this also requires adding update method to store historical data on source adapter.
return latestTimestamp();
}
}
116 changes: 113 additions & 3 deletions src/adapters/chainlink/ChainlinkSourceOracleAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@
pragma solidity 0.8.17;

import "../../interfaces/IBaseOracleAdapter.sol";
import "../../interfaces/chainlink/IAggregatorV3.sol";
import "../../interfaces/chainlink/IAggregatorV3Source.sol";

contract ChainlinkSourceOracleAdapter is IBaseOracleAdapter {
IAggregatorV3 public source;
struct RoundData {
uint80 roundId;
int256 answer;
uint256 startedAt;
uint256 updatedAt;
uint80 answeredInRound;
}

IAggregatorV3Source public source;

constructor(address _source) {
source = IAggregatorV3(_source);
source = IAggregatorV3Source(_source);
}

function latestAnswer() public view override returns (int256) {
Expand All @@ -22,4 +30,106 @@ contract ChainlinkSourceOracleAdapter is IBaseOracleAdapter {
function decimals() public view override returns (uint8) {
return source.decimals();
}

// Tries getting latest answer as of requested timestamp. If this is not available returns the current latest answer.
function tryLatestAnswerAt(
uint256 timestamp,
uint256 maxTraversal
) public view override returns (int256) {
return _tryLatestRoundDataAt(timestamp, maxTraversal).answer;
}

// Tries getting latest timestamp as of requested timestamp. If this is not available returns the current latest timestamp.
function tryLatestTimestampAt(
uint256 timestamp,
uint256 maxTraversal
) public view override returns (uint256) {
return _tryLatestRoundDataAt(timestamp, maxTraversal).updatedAt;
}

// Tries getting latest round data as of requested timestamp. If this is not available returns the current latest round data.
function _tryLatestRoundDataAt(
uint256 timestamp,
uint256 maxTraversal
)
internal
view
returns (RoundData memory)
{
(
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = source.latestRoundData();
RoundData memory latestRoundData = RoundData({
roundId: roundId,
answer: answer,
startedAt: startedAt,
updatedAt: updatedAt,
answeredInRound: answeredInRound
});
// In the happy path there have been no source updates since requested time, so we can return the latest data.
// We can use updatedAt property as it matches the block timestamp of the latest source transmission.
if (latestRoundData.updatedAt <= timestamp) return latestRoundData;

// Attempt traversing historical round data.
RoundData memory historicalRoundData = _searchRoundDataAt(timestamp, latestRoundData.roundId - 1, maxTraversal);

// Validate returned data. If it is not valid this means we failed to find data at requested timestamp so we
// fallback to returning the current latest round data.
if (historicalRoundData.updatedAt > 0) return historicalRoundData;
return latestRoundData;
}

// This returns uninitialized data if traversed all rounds of the current phase aggregator or exceeded maximum
// allowed round traversal. The caller should check if updatedAt is 0 to determine if the data is valid.
function _searchRoundDataAt(
uint256 timestamp,
uint80 roundId,
uint256 maxTraversal
)
internal
view
returns (RoundData memory)
{
RoundData memory roundData;
uint80 traversedRounds = 0;

while (traversedRounds < uint80(maxTraversal)) {
(
uint80 _roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) = source.getRoundData(roundId);
roundData = RoundData({
roundId: _roundId,
answer: answer,
startedAt: startedAt,
updatedAt: updatedAt,
answeredInRound: answeredInRound
});
// As per Chainlink documentation https://docs.chain.link/data-feeds/historical-data#roundid-in-proxy
// roundId on the aggregator proxy is composed of phaseId (higher 16 bits) and roundId from phase aggregator
// (lower 64 bits). The aggregator proxy does not keep track when its phase aggregators got switched. This
// means that we can only traverse rounds of the current phase aggregator. When phase aggregators are
// switched there is normally an overlap period when both new and old phase aggregators receive updates.
// Without knowing exact time when the aggregator proxy switched them we might end up returning historical
// data from the new phase aggregator that was not yet available on the aggregator proxy at the requested
// timestamp.
// Since phase aggregators are starting at round 1 we can detect unitialized round 0 (as in phase
// aggregator) in updatedAt and return unitialized round data that should be handled by the caller.
if (roundData.updatedAt <= timestamp || roundData.updatedAt == 0) return roundData;
roundId = roundData.roundId - 1;
traversedRounds++;
}

// Since we traversed past maximum allowed round traversal without finding latest data at requested timestamp,
// we must invalidate it so that the caller can fallback to the current latest data.
roundData.updatedAt = 0;
return roundData;
}
}
16 changes: 13 additions & 3 deletions src/adapters/compound/UniswapAnchoredViewSourceOracleAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity 0.8.17;

import "../../interfaces/IBaseOracleAdapter.sol";
import "../../interfaces/chainlink/IAggregatorV3.sol";
import "../../interfaces/chainlink/IAggregatorV3Source.sol";
import "../../interfaces/compound/IUniswapAnchoredView.sol";
import "../../interfaces/compound/ICToken.sol";
import "../../interfaces/compound/IValidatorProxy.sol";
Expand All @@ -14,7 +14,7 @@ contract UniswapAnchoredViewSourceOracleAdapter is IBaseOracleAdapter {

uint8 public decimals;

IAggregatorV3 public aggregator;
IAggregatorV3Source public aggregator;

constructor(IUniswapAnchoredView _source, address _cToken) {
source = _source;
Expand All @@ -24,7 +24,7 @@ contract UniswapAnchoredViewSourceOracleAdapter is IBaseOracleAdapter {
IUniswapAnchoredView.TokenConfig memory tokenConfig = source.getTokenConfigByCToken(address(cToken));
// TODO: make sure this does not change over time
(address current,,) = IValidatorProxy(tokenConfig.reporter).getAggregators();
aggregator = IAggregatorV3(current);
aggregator = IAggregatorV3Source(current);
}

function latestAnswer() public view override returns (int256) {
Expand All @@ -36,4 +36,14 @@ contract UniswapAnchoredViewSourceOracleAdapter is IBaseOracleAdapter {
function latestTimestamp() public view override returns (uint256) {
return aggregator.latestTimestamp();
}

function tryLatestAnswerAt(uint256 timestamp, uint256 maxTraversal) public view override returns (int256) {
// TODO: implement traversing source rounds to get latest data as of requested timestamp.
return latestAnswer();
}

function tryLatestTimestampAt(uint256 timestamp, uint256 maxTraversal) public view override returns (uint256) {
// TODO: implement traversing source rounds to get latest data as of requested timestamp.
return latestTimestamp();
}
}
12 changes: 12 additions & 0 deletions src/adapters/makerdao/ChronicleSourceOracleAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,16 @@ contract ChronicleSourceOracleAdapter is IBaseOracleAdapter {
function latestTimestamp() public view override returns (uint256) {
return source.zzz();
}

function tryLatestAnswerAt(uint256 timestamp, uint256 maxTraversal) public view override returns (int256) {
// TODO: implement traversing source rounds to get latest data as of requested timestamp.
// TODO: this also requires adding update method to store historical data on source adapter.
return latestAnswer();
}

function tryLatestTimestampAt(uint256 timestamp, uint256 maxTraversal) public view override returns (uint256) {
// TODO: implement traversing source rounds to get latest data as of requested timestamp.
// TODO: this also requires adding update method to store historical data on source adapter.
return latestTimestamp();
}
}
22 changes: 1 addition & 21 deletions src/controllers/BaseController.sol
Original file line number Diff line number Diff line change
@@ -1,25 +1,18 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.17;

import "openzeppelin-contracts/contracts/access/Ownable.sol";
import "../OevOracle.sol";
import "../interfaces/IBaseController.sol";

contract BaseController is IBaseController, OevOracle, Ownable {
contract BaseController is IBaseController, OevOracle {
constructor(address _sourceAdapter) OevOracle(_sourceAdapter) {}

mapping(address => bool) public updaters;

uint256 public permissionWindow = 10 minutes;

function setUpdater(address _updater, bool allowed) public onlyOwner {
updaters[_updater] = allowed;
}

function setPermissionWindow(uint256 _permissionWindow) public onlyOwner {
permissionWindow = _permissionWindow;
}

function canUpdate(address caller, uint256 cachedLatestTimestamp)
public
view
Expand All @@ -28,17 +21,4 @@ contract BaseController is IBaseController, OevOracle, Ownable {
{
return updaters[caller];
}

function canReturnCache(uint256 cachedLatestTimestamp, uint256 updateTimestamp)
public
view
override(IBaseController, OevOracle)
returns (bool)
{
// If cache was updated within the permission window, return the cached value.
if (block.timestamp - updateTimestamp < permissionWindow) return true;

// Otherwise check against last update time on source adapter.
return (sourceAdapter.latestTimestamp() - updateTimestamp < permissionWindow);
}
}
2 changes: 0 additions & 2 deletions src/interfaces/IBaseController.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,4 @@ pragma solidity 0.8.17;

interface IBaseController {
function canUpdate(address caller, uint256 cachedLatestTimestamp) external view returns (bool);

function canReturnCache(uint256 cachedLatestTimestamp, uint256 updateTimestamp) external view returns (bool);
}
4 changes: 4 additions & 0 deletions src/interfaces/IBaseOracleAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ interface IBaseOracleAdapter {
function latestTimestamp() external view returns (uint256);

function decimals() external view returns (uint8);

function tryLatestAnswerAt(uint256 timestamp, uint256 maxTraversal) external view returns (int256);

function tryLatestTimestampAt(uint256 timestamp, uint256 maxTraversal) external view returns (uint256);
}
2 changes: 2 additions & 0 deletions src/interfaces/IOevOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ interface IOevOracle {
function internalLatestAnswer() external view returns (int256);

function internalLatestTimestamp() external view returns (uint256);

function canReturnCache(uint256 updateTimestamp) external view returns (bool);
}
17 changes: 17 additions & 0 deletions src/interfaces/chainlink/IAggregatorV3Source.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// SPDX-License-Identifier: MIT
pragma solidity 0.8.17;

import "./IAggregatorV3.sol";

interface IAggregatorV3Source is IAggregatorV3 {

function latestRoundData()
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);

function getRoundData(uint80 _roundId)
external
view
returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound);
}
18 changes: 18 additions & 0 deletions test/mocks/MockSourceOracleAdapter.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ contract MockSourceOracleAdapter is IBaseOracleAdapter {
int256 public latestAnswer;
uint256 public latestTimestamp;
uint8 public decimals;
mapping(uint256 => int256) public _latestAnswerAt;
mapping(uint256 => uint256) public _latestTimestampAt;

constructor(uint8 _decimals) {
decimals = _decimals;
Expand All @@ -19,4 +21,20 @@ contract MockSourceOracleAdapter is IBaseOracleAdapter {
function setLatestTimestamp(uint256 _latestTimestamp) public {
latestTimestamp = _latestTimestamp;
}

function tryLatestAnswerAt(uint256 timestamp, uint256 maxTraversal) public view override returns (int256) {
return _latestAnswerAt[timestamp];
}

function tryLatestTimestampAt(uint256 timestamp, uint256 maxTraversal) public view override returns (uint256) {
return _latestTimestampAt[timestamp];
}

function setLatestAnswerAt(uint256 timestamp, int256 _latestAnswer) public {
_latestAnswerAt[timestamp] = _latestAnswer;
}

function setLatestTimestampAt(uint256 timestamp, uint256 _latestTimestamp) public {
_latestTimestampAt[timestamp] = _latestTimestamp;
}
}
Loading

0 comments on commit ff96808

Please sign in to comment.