Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
26 changes: 6 additions & 20 deletions contracts/oracles/SFrxETHOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,13 @@ import { ensureNonzeroAddress, ensureNonzeroValue } from "@venusprotocol/solidit
import { EXP_SCALE } from "@venusprotocol/solidity-utilities/contracts/constants.sol";
import { AccessControlledV8 } from "@venusprotocol/governance-contracts/contracts/Governance/AccessControlledV8.sol";
import { OracleInterface } from "../interfaces/OracleInterface.sol";
import { CappedOracle } from "./common/CappedOracle.sol";

/**
* @title SFrxETHOracle
* @author Venus
* @notice This oracle fetches the price of sfrxETH
*/
contract SFrxETHOracle is AccessControlledV8, OracleInterface, CappedOracle {
contract SFrxETHOracle is AccessControlledV8, OracleInterface {
/// @notice Address of SfrxEthFraxOracle
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
ISfrxEthFraxOracle public immutable SFRXETH_FRAX_ORACLE;
Expand All @@ -40,12 +39,7 @@ contract SFrxETHOracle is AccessControlledV8, OracleInterface, CappedOracle {
/// @notice Constructor for the implementation contract.
/// @custom:oz-upgrades-unsafe-allow constructor
/// @custom:error ZeroAddressNotAllowed is thrown when `_sfrxEthFraxOracle` or `_sfrxETH` are the zero address
constructor(
address _sfrxEthFraxOracle,
address _sfrxETH,
uint256 annualGrowthRate,
uint256 snapshotInterval
) CappedOracle(annualGrowthRate, snapshotInterval) {
constructor(address _sfrxEthFraxOracle, address _sfrxETH) {
ensureNonzeroAddress(_sfrxEthFraxOracle);
ensureNonzeroAddress(_sfrxETH);

Expand Down Expand Up @@ -82,23 +76,15 @@ contract SFrxETHOracle is AccessControlledV8, OracleInterface, CappedOracle {
}

/**
* @notice Address of the token
* @return token The address of the token
*/
function token() internal view override returns (address) {
return SFRXETH;
}

/**
* @notice Fetches the uncapped price of the token
* @param asset Address of the token
* @return price The price of the token in scaled decimal places
* @notice Fetches the USD price of sfrxETH
* @param asset Address of the sfrxETH token
* @return price The price scaled by 1e18
* @custom:error InvalidTokenAddress is thrown when the `asset` is not the sfrxETH token (`SFRXETH`)
* @custom:error BadPriceData is thrown if the `SFRXETH_FRAX_ORACLE` oracle informs it has bad data
* @custom:error ZeroValueNotAllowed is thrown if the prices (low or high, in USD) are zero
* @custom:error PriceDifferenceExceeded is thrown if priceHigh/priceLow is greater than `maxAllowedPriceDifference`
*/
function getUncappedPrice(address asset) internal view override returns (uint256) {
function getPrice(address asset) external view returns (uint256) {
if (asset != SFRXETH) revert InvalidTokenAddress();

(bool isBadData, uint256 priceLow, uint256 priceHigh) = SFRXETH_FRAX_ORACLE.getPrices();
Expand Down
120 changes: 0 additions & 120 deletions contracts/oracles/common/CappedOracle.sol

This file was deleted.

118 changes: 104 additions & 14 deletions contracts/oracles/common/CorrelatedTokenOracle.sol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused ensureNonzeroValue .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@
pragma solidity 0.8.25;

import { OracleInterface } from "../../interfaces/OracleInterface.sol";
import { ensureNonzeroAddress, ensureNonzeroValue } from "@venusprotocol/solidity-utilities/contracts/validators.sol";
import { ensureNonzeroAddress } from "@venusprotocol/solidity-utilities/contracts/validators.sol";
import { IERC20Metadata } from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
import { CappedOracle } from "./CappedOracle.sol";
import { Transient } from "../../lib/Transient.sol";

/**
* @title CorrelatedTokenOracle
* @notice This oracle fetches the price of a token that is correlated to another token.
*/
abstract contract CorrelatedTokenOracle is CappedOracle {
abstract contract CorrelatedTokenOracle {
/// Slot to cache the asset's price, used for transient storage
bytes32 public constant CACHE_SLOT = keccak256(abi.encode("venus-protocol/oracle/common/CappedOracle/cache"));

/// @notice Address of the correlated token
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable CORRELATED_TOKEN;
Expand All @@ -19,13 +22,33 @@ abstract contract CorrelatedTokenOracle is CappedOracle {
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
address public immutable UNDERLYING_TOKEN;

//// @notice Growth rate percentage in seconds. Ex: 1e18 is 100%
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
uint256 public immutable GROWTH_RATE_PER_SECOND;

/// @notice Snapshot update interval
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
uint256 public immutable SNAPSHOT_INTERVAL;

/// @notice Address of Resilient Oracle
/// @custom:oz-upgrades-unsafe-allow state-variable-immutable
OracleInterface public immutable RESILIENT_ORACLE;

/// @notice Last stored snapshot exchange rate
uint256 public snapshotExchangeRate;

/// @notice Last stored snapshot timestamp
uint256 public snapshotTimestamp;

/// @notice Emitted when the snapshot is updated
event SnapshotUpdated(uint256 exchangeRate, uint256 timestamp);

/// @notice Thrown if the token address is invalid
error InvalidTokenAddress();

/// @notice Thrown if the growth rate is invalid
error InvalidGrowthRate();

/// @notice Constructor for the implementation contract.
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(
Expand All @@ -34,39 +57,106 @@ abstract contract CorrelatedTokenOracle is CappedOracle {
address resilientOracle,
uint256 annualGrowthRate,
uint256 snapshotInterval
) CappedOracle(annualGrowthRate, snapshotInterval) {
) {
if ((annualGrowthRate == 0 && snapshotInterval > 0) || (annualGrowthRate > 0 && snapshotInterval == 0))
revert InvalidGrowthRate();

ensureNonzeroAddress(correlatedToken);
ensureNonzeroAddress(underlyingToken);
ensureNonzeroAddress(resilientOracle);

CORRELATED_TOKEN = correlatedToken;
UNDERLYING_TOKEN = underlyingToken;
RESILIENT_ORACLE = OracleInterface(resilientOracle);
SNAPSHOT_INTERVAL = snapshotInterval;
GROWTH_RATE_PER_SECOND = (annualGrowthRate) / (365 * 24 * 60 * 60);
}

/**
* @notice Returns if the price is capped
* @return isCapped Boolean indicating if the price is capped
*/
function isCapped() external view virtual returns (bool) {
uint256 maxAllowedExchangeRate = _getMaxAllowedExchangeRate();
if (maxAllowedExchangeRate == 0) {
return false;
}

uint256 exchangeRate = _getUnderlyingAmount();

return exchangeRate > maxAllowedExchangeRate;
}

/**
* @notice Updates the snapshot price and timestamp
*/
function updateSnapshot() public {
if (Transient.readCachedPrice(CACHE_SLOT, CORRELATED_TOKEN) != 0) {
return;
}
if (block.timestamp - snapshotTimestamp < SNAPSHOT_INTERVAL || SNAPSHOT_INTERVAL == 0) return;

uint256 exchangeRate = _getUnderlyingAmount();
uint256 maxAllowedExchangeRate = _getMaxAllowedExchangeRate();

snapshotExchangeRate = exchangeRate > maxAllowedExchangeRate ? maxAllowedExchangeRate : exchangeRate;
snapshotTimestamp = block.timestamp;
Transient.cachePrice(CACHE_SLOT, CORRELATED_TOKEN, snapshotExchangeRate);
emit SnapshotUpdated(snapshotExchangeRate, snapshotTimestamp);
}

/**
* @notice Fetches the price of the token
* @param asset Address of the token
* @return price The price of the token in scaled decimal places. It can be capped
* to a maximum value taking into account the growth rate
*/
function getPrice(address asset) public view returns (uint256) {
uint256 exchangeRate = Transient.readCachedPrice(CACHE_SLOT, asset);
if (exchangeRate != 0) {
return calculatePrice(asset, exchangeRate);
Comment on lines +115 to +117
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@web3rover can you please add a UT for the changes?

}

exchangeRate = _getUnderlyingAmount();

if (SNAPSHOT_INTERVAL == 0) {
return calculatePrice(asset, exchangeRate);
}

uint256 maxAllowedExchangeRate = _getMaxAllowedExchangeRate();

if ((exchangeRate > maxAllowedExchangeRate) && (maxAllowedExchangeRate != 0)) {
return calculatePrice(asset, maxAllowedExchangeRate);
} else {
return calculatePrice(asset, exchangeRate);
}
}

/**
* @notice Fetches the uncapped price of the correlated token
* @param asset Address of the correlated token
* @return price The price of the correlated token in scaled decimal places
* @notice Fetches price of the token based on an underlying exchange rate
* @param asset The address of the asset
* @param exchangeRate The underlying exchange rate to use
* @return price The price of the token in scaled decimal places
*/
function getUncappedPrice(address asset) internal view override returns (uint256) {
function calculatePrice(address asset, uint256 exchangeRate) internal view returns (uint256) {
if (asset != CORRELATED_TOKEN) revert InvalidTokenAddress();

uint256 underlyingAmount = _getUnderlyingAmount();
uint256 underlyingUSDPrice = RESILIENT_ORACLE.getPrice(UNDERLYING_TOKEN);

IERC20Metadata token = IERC20Metadata(CORRELATED_TOKEN);
uint256 decimals = token.decimals();

return (underlyingAmount * underlyingUSDPrice) / (10 ** decimals);
return (exchangeRate * underlyingUSDPrice) / (10 ** decimals);
}

/**
* @notice Address of the correlated token
* @return address Address of the correlated token
* @notice Gets the maximum allowed exchange rate for token
* @return maxPrice Maximum allowed price
*/
function token() internal view override returns (address) {
return CORRELATED_TOKEN;
function _getMaxAllowedExchangeRate() internal view returns (uint256) {
uint256 timeElapsed = block.timestamp - snapshotTimestamp;
uint256 maxPrice = snapshotExchangeRate + (snapshotExchangeRate * GROWTH_RATE_PER_SECOND * timeElapsed) / 1e18;
return maxPrice;
}

/**
Expand Down
Loading