Skip to content

Commit

Permalink
chore: add ReservoirPriceCache and ReservoirPriceOracle
Browse files Browse the repository at this point in the history
  • Loading branch information
xenide committed Mar 28, 2024
1 parent be167c0 commit 4119a6b
Show file tree
Hide file tree
Showing 5 changed files with 219 additions and 4 deletions.
174 changes: 174 additions & 0 deletions src/ReservoirPriceCache.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import { Owned } from "lib/amm-core/lib/solmate/src/auth/Owned.sol";
import { ReentrancyGuard } from "lib/amm-core/lib/solmate/src/utils/ReentrancyGuard.sol";
import { FixedPointMathLib } from "lib/amm-core/lib/solmate/src/utils/FixedPointMathLib.sol";

import { ReservoirPair } from "amm-core/ReservoirPair.sol";

import {
IReservoirPriceOracle,
OracleAverageQuery,
OracleAccumulatorQuery,
Variable
} from "src/interfaces/IReservoirPriceOracle.sol";

contract ReservoirPriceCache is Owned(msg.sender), ReentrancyGuard {
using FixedPointMathLib for uint256;

///////////////////////////////////////////////////////////////////////////////////////////////
// CONSTANTS //
///////////////////////////////////////////////////////////////////////////////////////////////

uint256 MAX_DEVIATION_THRESHOLD = 0.2e18; // 20%
uint256 MAX_TWAP_PERIOD = 1 hours;

///////////////////////////////////////////////////////////////////////////////////////////////
// EVENTS //
///////////////////////////////////////////////////////////////////////////////////////////////

event Oracle(address newOracle);
event TwapPeriod(uint256 newPeriod);
event PriceDeviationThreshold(uint256 newThreshold);
event RewardMultiplier(uint256 newMultiplier);
event Price(address indexed pair, uint256 price);

///////////////////////////////////////////////////////////////////////////////////////////////
// ERRORS //
///////////////////////////////////////////////////////////////////////////////////////////////

error RPC_THRESHOLD_TOO_HIGH();
error RPC_TWAP_PERIOD_TOO_HIGH();

///////////////////////////////////////////////////////////////////////////////////////////////
// STORAGE //
///////////////////////////////////////////////////////////////////////////////////////////////

IReservoirPriceOracle public oracle;

/// @notice percentage change greater than which, a price update with the oracles would succeed
/// 1e18 == 100%
uint64 public priceDeviationThreshold;

/// @notice percentage of gas fee the contract rewards the caller for updating the price
/// 1e18 == 100%
uint64 public rewardMultiplier;

/// @notice TWAP period for querying the oracle
uint64 public twapPeriod;

// for a certain pair, regardless of the curve id, the latest cached price of token1/token0
// calculate reciprocal to for price of token0/token1
mapping(address => uint256) public priceCache;

///////////////////////////////////////////////////////////////////////////////////////////////
// CONSTRUCTOR, FALLBACKS //
///////////////////////////////////////////////////////////////////////////////////////////////

constructor(IReservoirPriceOracle aOracle, uint64 aThreshold, uint64 aTwapPeriod, uint64 aMultiplier) {
updatePriceDeviationThreshold(aThreshold);
updateTwapPeriod(aTwapPeriod);
updateRewardMultiplier(aMultiplier);
}

/// @dev contract will hold native tokens to be distributed as gas bounty for updating the prices
/// anyone can contribute native tokens to this contract
receive() external payable { }

///////////////////////////////////////////////////////////////////////////////////////////////
// PUBLIC FUNCTIONS //
///////////////////////////////////////////////////////////////////////////////////////////////

// admin functions

function updateOracle(address aOracle) public onlyOwner {
oracle = IReservoirPriceOracle(aOracle);
}

function updatePriceDeviationThreshold(uint64 aNewThreshold) public onlyOwner {
if (aNewThreshold > MAX_DEVIATION_THRESHOLD) {
revert RPC_THRESHOLD_TOO_HIGH();
}

priceDeviationThreshold = aNewThreshold;
emit PriceDeviationThreshold(aNewThreshold);
}

function updateTwapPeriod(uint64 aNewPeriod) public onlyOwner {
if (aNewPeriod > MAX_TWAP_PERIOD) {
revert RPC_TWAP_PERIOD_TOO_HIGH();
}
twapPeriod = aNewPeriod;
emit TwapPeriod(aNewPeriod);
}

function updateRewardMultiplier(uint64 aNewMultiplier) public onlyOwner {
rewardMultiplier = aNewMultiplier;
emit RewardMultiplier(aNewMultiplier);
}

// oracle price functions

function getPriceForPair(address aPair) external view returns (uint256) {
return priceCache[aPair];
}

// price update related functions

function isPriceUpdateIncentivized() external view returns (bool) {
return address(this).balance > 0;
}

function gasBountyAvailable() external view returns (uint256) {
return address(this).balance;
}

/// @dev we do not allow specifying which address gets the reward, to save on calldata gas
function updatePrice(address aPair) external nonReentrant {
ReservoirPair lPair = ReservoirPair(aPair);

OracleAverageQuery[] memory lQueries;
lQueries[0] = OracleAverageQuery(
Variable.RAW_PRICE,
address(lPair.token0()),
address(lPair.token1()),
twapPeriod,
0 // now
);

// reads new price from pair
uint256 lNewPrice = oracle.getTimeWeightedAverage(lQueries)[0];

// determine if price has moved beyond the threshold
// reward caller if so
if (_calcPercentageDiff(lNewPrice, priceCache[aPair]) >= priceDeviationThreshold) {
_rewardUpdater(msg.sender);
}

priceCache[aPair] = lNewPrice;
emit Price(aPair, lNewPrice);
}

///////////////////////////////////////////////////////////////////////////////////////////////
// INTERNAL FUNCTIONS //
///////////////////////////////////////////////////////////////////////////////////////////////

// TODO: replace this with safe, audited lib function
function _calcPercentageDiff(uint256 aOriginal, uint256 aNew) internal pure returns (uint256) {
if (aOriginal > aNew) {
return (aOriginal - aNew) * 1e18 / aOriginal;
} else {
return (aNew - aOriginal) * 1e18 / aOriginal;
}
}

function _rewardUpdater(address lRecipient) internal {
// TODO: make sure this works on L1 as well as L2s
uint256 lPayoutAmt = block.basefee.mulWadDown(rewardMultiplier);

if (lPayoutAmt <= address(this).balance) {
payable(lRecipient).transfer(lPayoutAmt);
} else { } // do nothing if lPayoutAmt is greater than the balance
}
}
26 changes: 26 additions & 0 deletions src/ReservoirPriceOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;

import { IReservoirPriceOracle, OracleAverageQuery, OracleLatestQuery, OracleAccumulatorQuery, Variable } from "src/interfaces/IReservoirPriceOracle.sol";

contract ReservoirPriceOracle is IReservoirPriceOracle {
function getTimeWeightedAverage(OracleAverageQuery[] memory aQueries)
external
view
returns (uint256[] memory rResults)
{ }

function getLatest(OracleLatestQuery calldata aQuery) external view returns (uint256) {
return 0;
}

function getLargestSafeQueryWindow() external view returns (uint256) {
return 0;
}

function getPastAccumulators(OracleAccumulatorQuery[] memory aQueries)
external
view
returns (int256[] memory rResults)
{ }
}
15 changes: 15 additions & 0 deletions src/Structs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,31 @@ import { Variable } from "src/Enums.sol";
*/
struct OracleAverageQuery {
Variable variable;
address base;
address quote;
uint256 secs;
uint256 ago;
}

/**
* @dev Information for a query for the latest variable
*
* TODO: fill this in
*/
struct OracleLatestQuery {
Variable variable;
address base;
address quote;
}

/**
* @dev Information for an Accumulator query.
*
* Each query estimates the accumulator at a time `ago` seconds ago.
*/
struct OracleAccumulatorQuery {
Variable variable;
address base;
address quote;
uint256 ago;
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

pragma solidity ^0.8.0;

import { OracleAverageQuery, OracleAccumulatorQuery } from "src/Structs.sol";
import { OracleAverageQuery, OracleLatestQuery, OracleAccumulatorQuery } from "src/Structs.sol";
import { Variable } from "src/Enums.sol";

/**
Expand All @@ -27,7 +27,7 @@ import { Variable } from "src/Enums.sol";
* Once the oracle is fully initialized, all queries are guaranteed to succeed as long as they require no data that
* is not older than the largest safe query window.
*/
interface IPriceOracle {
interface IReservoirPriceOracle {
/**
* @dev Returns the time average weighted price corresponding to each of `queries`. Prices are represented as 18
* decimal fixed point values.
Expand All @@ -40,7 +40,7 @@ interface IPriceOracle {
/**
* @dev Returns latest sample of `variable`. Prices are represented as 18 decimal fixed point values.
*/
function getLatest(Variable variable) external view returns (uint256);
function getLatest(OracleLatestQuery calldata variable) external view returns (uint256);

/**
* @dev Returns largest time window that can be safely queried, where 'safely' means the Oracle is guaranteed to be
Expand Down
2 changes: 1 addition & 1 deletion src/libraries/QueryProcessor.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { LogCompression } from "amm-core/libraries/LogCompression.sol";
import { Buffer } from "amm-core/libraries/Buffer.sol";
import { ReservoirPair, Observation } from "amm-core/ReservoirPair.sol";

import { IPriceOracle, Variable, OracleAverageQuery, OracleAccumulatorQuery } from "src/interfaces/IPriceOracle.sol";
import { Variable, OracleAverageQuery, OracleAccumulatorQuery } from "src/interfaces/IReservoirPriceOracle.sol";
import { BadVariableRequest, OracleNotInitialized, InvalidSeconds, QueryTooOld, BadSecs } from "src/Errors.sol";
import { Samples } from "src/libraries/Samples.sol";

Expand Down

0 comments on commit 4119a6b

Please sign in to comment.