Skip to content

Commit

Permalink
RToken Oracle
Browse files Browse the repository at this point in the history
  • Loading branch information
akshatmittal committed Jun 22, 2023
1 parent ba0c85a commit fee0cb1
Show file tree
Hide file tree
Showing 6 changed files with 64 additions and 154 deletions.
19 changes: 19 additions & 0 deletions contracts/interfaces/IRTokenOracle.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
// SPDX-License-Identifier: BlueOak-1.0.0
pragma solidity 0.8.19;

// RToken Oracle Interface
interface IRTokenOracle {
struct CachedOracleData {
uint192 cachedPrice;
uint256 cachedAtTime;
uint48 cachedAtNonce;
uint48 cachedTradesOpen;
uint256 cachedTradesNonce;
}

// @returns rTokenPrice {D18} {UoA/tok} The price of the RToken, in UoA
function latestPrice() external returns (uint192 rTokenPrice, uint256 updatedAt);

// Force recalculate the price of the RToken
function forceUpdatePrice() external;
}
3 changes: 3 additions & 0 deletions contracts/interfaces/ITrading.sol
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ interface ITrading is IComponent, IRewardableComponent {

/// @return The number of ongoing trades open
function tradesOpen() external view returns (uint48);

/// @return The number of total trades ever opened
function tradesNonce() external view returns (uint256);
}

interface TestITrading is ITrading {
Expand Down
5 changes: 5 additions & 0 deletions contracts/p0/mixins/Trading.sol
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ abstract contract TradingP0 is RewardableP0, ITrading {

uint192 public minTradeVolume; // {UoA}

// === 3.0.0 ===
uint256 public tradesNonce; // to keep track of how many trades have been opened in total

// untestable:
// `else` branch of `onlyInitializing` (ie. revert) is currently untestable.
// This function is only called inside other `init` functions, each of which is wrapped
Expand Down Expand Up @@ -68,6 +71,8 @@ abstract contract TradingP0 is RewardableP0, ITrading {
trade = broker.openTrade(kind, req);
trades[req.sell.erc20()] = trade;
tradesOpen++;
tradesNonce++;

emit TradeStarted(
trade,
req.sell.erc20(),
Expand Down
98 changes: 0 additions & 98 deletions contracts/p1/RTokenOracle.sol

This file was deleted.

8 changes: 5 additions & 3 deletions contracts/p1/mixins/Trading.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,11 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl

// === Governance param ===
uint192 public maxTradeSlippage; // {%}

uint192 public minTradeVolume; // {UoA}

// === 3.0.0 ===
uint256 public tradesNonce; // to keep track of how many trades have been opened in total

// ==== Invariants ====
// tradesOpen = len(values(trades))
// trades[sell] != 0 iff trade[sell] has been opened and not yet settled
Expand Down Expand Up @@ -111,7 +113,6 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl
// trades' = trades.set(req.sell, tradeID)
// tradesOpen' = tradesOpen + 1
function tryTrade(TradeKind kind, TradeRequest memory req) internal returns (ITrade trade) {
/* */
IERC20 sell = req.sell.erc20();
assert(address(trades[sell]) == address(0));

Expand All @@ -121,6 +122,7 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl
trade = broker.openTrade(kind, req);
trades[sell] = trade;
tradesOpen++;
tradesNonce++;

emit TradeStarted(trade, sell, req.buy.erc20(), req.sellAmount, req.minBuyAmount);
}
Expand All @@ -146,5 +148,5 @@ abstract contract TradingP1 is Multicall, ComponentP1, ReentrancyGuardUpgradeabl
* variables without shifting down storage in the inheritance chain.
* See https://docs.openzeppelin.com/contracts/4.x/upgradeable#storage_gaps
*/
uint256[46] private __gap;
uint256[45] private __gap;
}
85 changes: 32 additions & 53 deletions contracts/plugins/assets/RTokenAsset.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,20 @@ pragma solidity 0.8.19;
import "../../p1/mixins/RecollateralizationLib.sol";
import "../../interfaces/IMain.sol";
import "../../interfaces/IRToken.sol";
import "../../interfaces/IRTokenOracle.sol";
import "./Asset.sol";
import "./VersionedAsset.sol";

// This interface is here temporarily
interface ModifiedChainlinkInterface {
function latestAnswer() external returns (int256);

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

uint256 constant ORACLE_TIMEOUT = 15 minutes;

/// Once an RToken gets large enough to get a price feed, replacing this asset with
/// a simpler one will do wonders for gas usage
contract RTokenAsset is IAsset, VersionedAsset, ModifiedChainlinkInterface {
contract RTokenAsset is IAsset, VersionedAsset, IRTokenOracle {
using FixLib for uint192;
using OracleLib for AggregatorV3Interface;

// Component addresses are not mutable in protocol, so it's safe to cache these
// IMain public immutable main;
IMain public immutable main;
IBasketHandler public immutable basketHandler;
IAssetRegistry public immutable assetRegistry;
IBackingManager public immutable backingManager;
Expand All @@ -43,16 +29,14 @@ contract RTokenAsset is IAsset, VersionedAsset, ModifiedChainlinkInterface {
uint192 public immutable maxTradeVolume; // {UoA}

// Oracle State
int256 public cachedPrice;
uint256 public cachedAtTime;
uint48 public cachedAtNonce;
CachedOracleData public cachedOracleData;

/// @param maxTradeVolume_ {UoA} The max trade volume, in UoA
constructor(IRToken erc20_, uint192 maxTradeVolume_) {
require(address(erc20_) != address(0), "missing erc20");
require(maxTradeVolume_ > 0, "invalid max trade volume");

IMain main = erc20_.main();
main = erc20_.main();
basketHandler = main.basketHandler();
assetRegistry = main.assetRegistry();
backingManager = main.backingManager();
Expand Down Expand Up @@ -90,7 +74,7 @@ contract RTokenAsset is IAsset, VersionedAsset, ModifiedChainlinkInterface {
function refresh() public virtual override {
// No need to save lastPrice; can piggyback off the backing collateral's lotPrice()

cachedAtTime = 0; // force oracle refresh
cachedOracleData.cachedAtTime = 0; // force oracle refresh
}

// solhint-enable no-empty-blocks
Expand Down Expand Up @@ -158,44 +142,40 @@ contract RTokenAsset is IAsset, VersionedAsset, ModifiedChainlinkInterface {
_updateCachedPrice();
}

// ==== Private ====

// Update Oracle price
function _updateCachedPrice() internal {
(uint192 low, uint192 high) = price();

require(low != 0 && high != FIX_MAX, "invalid price");

cachedPrice = int256((uint256(low) + uint256(high)) / 2);
cachedAtTime = block.timestamp;
cachedAtNonce = basketHandler.nonce();
}

function latestRoundData()
external
returns (
uint80 roundId,
int256 answer,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
)
{
function latestPrice() external returns (uint192 rTokenPrice, uint256 updatedAt) {
// Situations that require an update, from most common to least common.
if (
cachedAtTime + ORACLE_TIMEOUT <= block.timestamp || // Cache Timeout
cachedAtNonce != basketHandler.nonce() // Basket nonce was updated
// !basketHandler.fullyCollateralized() // Basket is not fully collateralized
// Basket is recapitalizing, but there's not enough RSR.
cachedOracleData.cachedAtTime + ORACLE_TIMEOUT <= block.timestamp || // Cache Timeout
cachedOracleData.cachedAtNonce != basketHandler.nonce() || // Basket nonce was updated
cachedOracleData.cachedTradesNonce != backingManager.tradesNonce() || // New trades were started..

Check failure on line 150 in contracts/plugins/assets/RTokenAsset.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Line length must be no more than 100 but current length is 110
cachedOracleData.cachedTradesOpen != backingManager.tradesOpen() // ..or settled (between updates)

Check failure on line 151 in contracts/plugins/assets/RTokenAsset.sol

View workflow job for this annotation

GitHub Actions / Lint Checks

Line length must be no more than 100 but current length is 110
) {
_updateCachedPrice();
}

return (0, cachedPrice, 0, cachedAtTime, 0);
return (cachedOracleData.cachedPrice, cachedOracleData.cachedAtTime);
}

function latestAnswer() external returns (int256 latestPrice) {
(, latestPrice, , , ) = this.latestRoundData();
// ==== Private ====

// Update Oracle Data
function _updateCachedPrice() internal {
if (cachedOracleData.cachedAtTime == block.timestamp) {
// The price was updated in the same block.
return;
}

(uint192 low, uint192 high) = price();

require(low != 0 && high != FIX_MAX, "invalid price");

cachedOracleData = CachedOracleData(
(low + high) / 2,
block.timestamp,
basketHandler.nonce(),
backingManager.tradesOpen(),
backingManager.tradesNonce()
);
}

/// Computationally expensive basketRange calculation; used in price() & lotPrice()
Expand All @@ -213,7 +193,6 @@ contract RTokenAsset is IAsset, VersionedAsset, ModifiedChainlinkInterface {
// the absence of an external price feed. Any RToken that gets reasonably big
// should switch over to an asset with a price feed.

IMain main = backingManager.main();
TradingContext memory ctx;

ctx.basketsHeld = basketsHeld;
Expand Down

0 comments on commit fee0cb1

Please sign in to comment.