diff --git a/src/ReservoirPriceCache.sol b/src/ReservoirPriceCache.sol new file mode 100644 index 0000000..eed8c66 --- /dev/null +++ b/src/ReservoirPriceCache.sol @@ -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 + } +} diff --git a/src/ReservoirPriceOracle.sol b/src/ReservoirPriceOracle.sol new file mode 100644 index 0000000..779814e --- /dev/null +++ b/src/ReservoirPriceOracle.sol @@ -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) + { } +} diff --git a/src/Structs.sol b/src/Structs.sol index 76a3085..fb03eb3 100644 --- a/src/Structs.sol +++ b/src/Structs.sol @@ -12,10 +12,23 @@ 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. * @@ -23,5 +36,7 @@ struct OracleAverageQuery { */ struct OracleAccumulatorQuery { Variable variable; + address base; + address quote; uint256 ago; } diff --git a/src/interfaces/IPriceOracle.sol b/src/interfaces/IReservoirPriceOracle.sol similarity index 91% rename from src/interfaces/IPriceOracle.sol rename to src/interfaces/IReservoirPriceOracle.sol index bcffb44..d9d79ef 100644 --- a/src/interfaces/IPriceOracle.sol +++ b/src/interfaces/IReservoirPriceOracle.sol @@ -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"; /** @@ -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. @@ -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 diff --git a/src/libraries/QueryProcessor.sol b/src/libraries/QueryProcessor.sol index 22a42b5..d537894 100644 --- a/src/libraries/QueryProcessor.sol +++ b/src/libraries/QueryProcessor.sol @@ -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";