Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add USD price feed support for PriceFeedOracle #1271

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2e1de7b
feat: add IPriceFeedOracle.AggregatorType enum
rackstar Oct 25, 2024
589ab7c
refactor: rename mapping to to assetsMap and implement own assets get…
rackstar Oct 25, 2024
ebec9db
feat: add ETH conversion for USD aggregatorTypes
rackstar Oct 25, 2024
314abb4
test: fix PriceFeedOracleMock contract
rackstar Oct 25, 2024
dd72ac1
test: add PriceFeedOracle cbBTC (USD-based aggregator) ETH conversion…
rackstar Oct 25, 2024
7bb5200
test: fix basic-functionality-tests PriceFeedOracle constructor update
rackstar Oct 25, 2024
b5a6887
test: update fork utils constants
rackstar Oct 25, 2024
1e74780
test: add add-asset-cbbtc fork tests
rackstar Oct 25, 2024
23ec93d
test: fix fork tests - drop deprecated ProductsV1 contract
rackstar Oct 25, 2024
d367812
fix: add new require and fix existing params length require
rackstar Oct 25, 2024
33910a8
chore: update package version to 2.10.0
rackstar Oct 25, 2024
35fe804
test: fix unit/integration tests
rackstar Oct 25, 2024
c00920e
test: clean up tests
rackstar Oct 25, 2024
2326a27
refactor: rename OracleAsset struct to AssetInfo
rackstar Oct 30, 2024
9e1fe93
chore: cleanup imports in PriceFeedOracle tests
shark0der Oct 30, 2024
cbb8bba
chore: refactor getEthForAsset unit test
shark0der Oct 30, 2024
253eceb
chore: refactor getAssetForEth unit test
shark0der Oct 30, 2024
b8d9a07
chore: refactor getAssetToEthRate unit test
shark0der Oct 30, 2024
f451aa9
refactor: require ethUsd asset on PriceFeedOracle constructor
rackstar Oct 30, 2024
062aff7
test: add AggregatorType to lib/constants
rackstar Oct 30, 2024
ca1ad5d
test: fix unit/integration tests
rackstar Oct 30, 2024
268d6db
refactor: drop redundant run-time aggregator zero address check
rackstar Nov 4, 2024
cacff71
feat: add PriceFeedOracle.assetsMap getter interface
rackstar Nov 4, 2024
1e1986d
docs: add natspec and clean up comments
rackstar Nov 4, 2024
c0c4727
test: add missing assetsMap to PriceFeedOracleGeneric mock contract
rackstar Nov 4, 2024
78a1130
test: fix unit test - add value to ChainlinkAggregatorMock.decimals
rackstar Nov 4, 2024
b9bcf25
test: drop deprecated unit test
rackstar Nov 4, 2024
05e40cd
feat: add assetDecimals non zero validation on PriceFeedOracle constr…
rackstar Nov 4, 2024
8909174
feat: add aggregator.decimals validation on PriceFeedOracle constructor
rackstar Nov 4, 2024
c51cd54
refactor: convert require to custom errors
rackstar Nov 4, 2024
02c85ad
docs: add USD to ETH rate code comments
rackstar Nov 5, 2024
827af7b
test: update add cbBTC asset parameters in fork test
rackstar Nov 5, 2024
575781b
refactor: rename InvalidAggregatorDecimals to IncompatibleAggregatorD…
rackstar Nov 5, 2024
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
18 changes: 17 additions & 1 deletion contracts/interfaces/IPriceFeedOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,32 @@ interface Aggregator {

interface IPriceFeedOracle {

struct OracleAsset {
enum AggregatorType { ETH, USD }

struct AssetInfo {
Aggregator aggregator;
AggregatorType aggregatorType;
uint8 decimals;
}

function ETH() external view returns (address);
function assets(address) external view returns (Aggregator, uint8);
function assetsMap(address) external view returns (Aggregator, AggregatorType, uint8);

function getAssetToEthRate(address asset) external view returns (uint);
function getAssetForEth(address asset, uint ethIn) external view returns (uint);
function getEthForAsset(address asset, uint amount) external view returns (uint);

/* ========== ERRORS ========== */

error EmptyAssetAddresses();
error ArgumentLengthMismatch(uint assetAddressesLength, uint aggregatorsLength, uint typesLength, uint decimalsLength);
error ZeroAddress(string parameter);
error ZeroDecimals(address asset);
error IncompatibleAggregatorDecimals(address aggregator, uint8 aggregatorDecimals, uint8 expectedDecimals);
error UnknownAggregatorType(uint8 aggregatorType);
error EthUsdAggregatorNotSet();
error InvalidEthAggregatorType(AggregatorType actual, AggregatorType expected);
error UnknownAsset(address asset);
error NonPositiveRate(address aggregator, int rate);
}
2 changes: 1 addition & 1 deletion contracts/mocks/common/ChainlinkAggregatorMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ pragma solidity ^0.8.18;
contract ChainlinkAggregatorMock {

uint public latestAnswer;
uint public decimals;
uint public decimals = 18;

function setDecimals(uint _decimals) public {
decimals = _decimals;
Expand Down
7 changes: 6 additions & 1 deletion contracts/mocks/common/PriceFeedOracleMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import "../../interfaces/IPriceFeedOracle.sol";
contract PriceFeedOracleMock is IPriceFeedOracle {

address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
mapping(address => OracleAsset) public assets;
mapping(address => AssetInfo) public assetsMap;

uint public ethRate;

Expand All @@ -26,4 +26,9 @@ contract PriceFeedOracleMock is IPriceFeedOracle {
function getEthForAsset(address, uint amount) external view returns (uint) {
return amount / ethRate;
}

function assets(address assetAddress) external view returns (Aggregator, uint8) {
AssetInfo memory asset = assetsMap[assetAddress];
return (asset.aggregator, asset.decimals);
}
}
4 changes: 4 additions & 0 deletions contracts/mocks/generic/PriceFeedOracleGeneric.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ contract PriceFeedOracleGeneric is IPriceFeedOracle {
revert("Unsupported");
}

function assetsMap(address) external virtual view returns (Aggregator, AggregatorType, uint8) {
revert("Unsupported");
}

function getAssetToEthRate(address) external virtual view returns (uint) {
revert("Unsupported");
}
Expand Down
144 changes: 100 additions & 44 deletions contracts/modules/capital/PriceFeedOracle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,93 +4,149 @@ pragma solidity ^0.8.18;

import "../../interfaces/IPriceFeedOracle.sol";


contract PriceFeedOracle is IPriceFeedOracle {

mapping(address => OracleAsset) public assets;
mapping(address => AssetInfo) public assetsMap;

address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
address public immutable safeTracker;

constructor(
address[] memory _assetAddresses,
address[] memory _assetAggregators,
AggregatorType[] memory _aggregatorTypes,
uint8[] memory _assetDecimals,
address _safeTracker
) {
require(
_assetAddresses.length == _assetAggregators.length && _assetAggregators.length == _assetDecimals.length,
"PriceFeedOracle: different args length"
);
require(_safeTracker != address(0), "PriceFeedOracle: safeTracker cannot be zero address");
if (_assetAddresses.length == 0) {
revert EmptyAssetAddresses();
}
if (
_assetAddresses.length != _assetAggregators.length ||
_assetAggregators.length != _aggregatorTypes.length ||
_aggregatorTypes.length != _assetDecimals.length
) {
revert ArgumentLengthMismatch(
_assetAddresses.length,
_assetAggregators.length,
_aggregatorTypes.length,
_assetDecimals.length
);
}
if (_safeTracker == address(0)) {
revert ZeroAddress("safeTracker");
}

safeTracker = _safeTracker;
assets[_safeTracker] = OracleAsset(Aggregator(_safeTracker), 18);
assetsMap[_safeTracker] = AssetInfo(Aggregator(_safeTracker), AggregatorType.ETH, 18);

for (uint i = 0; i < _assetAddresses.length; i++) {
assets[_assetAddresses[i]] = OracleAsset(Aggregator(_assetAggregators[i]), _assetDecimals[i]);
if (_assetAddresses[i] == address(0)) {
revert ZeroAddress("assetAddress");
}
if (_assetAggregators[i] == address(0)) {
revert ZeroAddress("aggregator");
}
if (_assetDecimals[i] == 0) {
revert ZeroDecimals(_assetAddresses[i]);
}

Aggregator aggregator = Aggregator(_assetAggregators[i]);
uint8 aggregatorDecimals = aggregator.decimals();

if (_aggregatorTypes[i] == AggregatorType.ETH && aggregatorDecimals != 18) {
revert IncompatibleAggregatorDecimals(_assetAggregators[i], aggregatorDecimals, 18);
}
if (_aggregatorTypes[i] == AggregatorType.USD && aggregatorDecimals != 8) {
revert IncompatibleAggregatorDecimals(_assetAggregators[i], aggregatorDecimals, 8);
}

assetsMap[_assetAddresses[i]] = AssetInfo(aggregator, _aggregatorTypes[i], _assetDecimals[i]);
}

// Require ETH-USD asset
AssetInfo memory ethAsset = assetsMap[ETH];
if (address(ethAsset.aggregator) == address(0)) {
revert EthUsdAggregatorNotSet();
}
if (ethAsset.aggregatorType != AggregatorType.USD) {
revert InvalidEthAggregatorType(ethAsset.aggregatorType, AggregatorType.USD);
}
}

/**
* @dev Returns the amount of ether in wei that are equivalent to 1 unit (10 ** decimals) of asset
* @param assetAddress address of asset
* @return price in ether
*/
/// @notice Returns the amount of ether in wei that are equivalent to 1 unit (10 ** decimals) of asset
/// @param assetAddress address of asset
/// @return price in ether
function getAssetToEthRate(address assetAddress) public view returns (uint) {
if (assetAddress == ETH || assetAddress == safeTracker) {
return 1 ether;
}

OracleAsset memory asset = assets[assetAddress];
return _getAssetToEthRate(asset.aggregator);
AssetInfo memory asset = assetsMap[assetAddress];
return _getAssetToEthRate(asset.aggregator, asset.aggregatorType);
}

/**
* @dev Returns the amount of currency that is equivalent to ethIn amount of ether.
* @param assetAddress address of asset
* @param ethIn amount of ether to be converted to the asset
* @return asset amount
*/
/// @notice Returns the amount of currency that is equivalent to ethIn amount of ether.
/// @param assetAddress address of asset
/// @param ethIn amount of ether to be converted to the asset
/// @return asset amount
function getAssetForEth(address assetAddress, uint ethIn) external view returns (uint) {
if (assetAddress == ETH || assetAddress == safeTracker) {
return ethIn;
}

OracleAsset memory asset = assets[assetAddress];
uint price = _getAssetToEthRate(asset.aggregator);
AssetInfo memory asset = assetsMap[assetAddress];
uint price = _getAssetToEthRate(asset.aggregator, asset.aggregatorType);

return ethIn * (10**uint(asset.decimals)) / price;
return ethIn * (10 ** uint(asset.decimals)) / price;
}

/**
* @dev Returns the amount of eth that is equivalent to a given asset and amount
* @param assetAddress address of asset
* @param amount amount of asset
* @return amount of ether
*/
/// @notice Returns the amount of eth that is equivalent to a given asset and amount
/// @param assetAddress address of asset
/// @param amount amount of asset
/// @return amount of ether
function getEthForAsset(address assetAddress, uint amount) external view returns (uint) {
if (assetAddress == ETH || assetAddress == safeTracker) {
return amount;
}

OracleAsset memory asset = assets[assetAddress];
uint price = _getAssetToEthRate(asset.aggregator);
AssetInfo memory asset = assetsMap[assetAddress];
uint price = _getAssetToEthRate(asset.aggregator, asset.aggregatorType);

return amount * (price) / 10**uint(asset.decimals);
return amount * (price) / 10 ** uint(asset.decimals);
}

/**
* @dev Returns the amount of ether in wei that are equivalent to 1 unit (10 ** decimals) of asset
* @param aggregator The asset aggregator
* @return price in ether
*/
function _getAssetToEthRate(Aggregator aggregator) internal view returns (uint) {
require(address(aggregator) != address(0), "PriceFeedOracle: Unknown asset");
// TODO: consider checking the latest timestamp and revert if it's *very* old
/// @notice Returns the amount of ether in wei that are equivalent to 1 unit (10 ** decimals) of asset
/// @param aggregator The asset aggregator
/// @param aggregatorType The asset aggregator type (i.e ETH, USD)
/// @return price in ether
function _getAssetToEthRate(Aggregator aggregator, AggregatorType aggregatorType) internal view returns (uint) {
// NOTE: Current implementation relies on off-chain staleness checks, consider adding on-chain staleness check?
int rate = aggregator.latestAnswer();
require(rate > 0, "PriceFeedOracle: Rate must be > 0");
if (rate <= 0) {
revert NonPositiveRate(address(aggregator), rate);
}

if (aggregatorType == AggregatorType.ETH) {
return uint(rate);
}

// AggregatorType.USD - convert the USD rate to its equivalent ETH rate using the ETH-USD exchange rate
AssetInfo memory ethAsset = assetsMap[ETH];

int ethUsdRate = ethAsset.aggregator.latestAnswer();
if (ethUsdRate <= 0) {
revert NonPositiveRate(ETH, ethUsdRate);
}

return (uint(rate) * 1e18) / uint(ethUsdRate);
}

return uint(rate);
/// @notice Retrieves the aggregator and decimals for a specific asset
/// @param assetAddress address of the asset
/// @return Aggregator instance and decimals of the asset
function assets(address assetAddress) external view returns (Aggregator, uint8) {
AssetInfo memory asset = assetsMap[assetAddress];
return (asset.aggregator, asset.decimals);
}
}
6 changes: 6 additions & 0 deletions lib/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,11 @@ const ContractCode = {
CoverProducts: 'CP',
};

const AggregatorType = {
ETH: 0,
USD: 1,
};

module.exports = {
Assets,
CoverStatus,
Expand All @@ -171,4 +176,5 @@ module.exports = {
NXMasterOwnerParamType,
PoolAsset,
ContractCode,
AggregatorType,
};
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "nexusmutual",
"version": "2.9.0",
"version": "2.10.0",
"description": "NexusMutual smart contracts",
"repository": {
"type": "git",
Expand Down
Loading
Loading