Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
114 changes: 114 additions & 0 deletions src/feeds/ChainlinkBridgeAssetFeed.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
pragma solidity ^0.8.20;

interface IChainlinkFeed {
function aggregator() external view returns (address aggregator);
function decimals() external view returns (uint8 decimals);
function latestRoundData()
external
view
returns (
uint80 roundId,
int256 crvUsdPrice,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
);
function latestAnswer() external view returns (int256 price);
}
interface IAggregator {
function maxAnswer() external view returns (int192);
function minAnswer() external view returns (int192);
}
interface ICurvePool {
function price_oracle(uint k) external view returns (uint256);
}

contract ChainlinkBridgeAssetFeed {
IChainlinkFeed public immutable collateralToBridgeAsset;
IChainlinkFeed public immutable bridgeAssetToUsd;
bool public immutable bridgeAssetDenominator;
string public description;

/**
* @notice Oracle for the USD price of a collateral asset derived by combining a collateral-bridgeAsset oracle and bridgeAsset-USD oracle
* @param _collateralToBridgeAsset Chainlink oracle returning the collateral/bridgeAsset OR bridgeAsset/collateral price
* @param _bridgeAssetToUsd Chainlink oracle returning the bridgeAsset/USD price
* @param _bridgeAssetDenominator If true, the `_collateralToBridgeAsset` oracle will return collateral/bridgeAsset, if false, bridgeAsset/collateral.
* @dev We assume the underlying oracles have already been normalized using our standard chainlink feed. These feeds should also be used for fallback logic.
*/
constructor(address _collateralToBridgeAsset, address _bridgeAssetToUsd, bool _bridgeAssetDenominator){
collateralToBridgeAsset = IChainlinkFeed(_collateralToBridgeAsset);
bridgeAssetToUsd = IChainlinkFeed(_bridgeAssetToUsd);
bridgeAssetDenominator = _bridgeAssetDenominator;
//TODO: Do string concat
description = "Combine oracle of 2 underlying oracles";
require(collateralToBridgeAsset.decimals() == 18, "collateralToBridgeAsset feed not normalized");
require(bridgeAssetToUsd.decimals() == 18, "bridgeAssetToUsd feed not normalize");
}

function decimals() external view returns (uint8) {
return 18;
}

/**
* @notice Retrieves the latest round data for the collateral token price feed
* @dev This function calculates the collateral price in USD by combining the bridgeAsset to USD price from a Chainlink oracle and the collateral to bridgeAsset ratio from the bridgeAsset Chainlink oracle
* @return roundId The round ID of the Chainlink price feed for the feed with the lowest updatedAt feed
* @return bridgeAssetToUsdPrice The latest collateral price in USD computed from the collateral/bridgeAsset and bridgeAsset/USD feeds
* @return startedAt The timestamp when the latest round of Chainlink price feed started of the lowest last updatedAt feed
* @return updatedAt The lowest timestamp when either of the latest round of Chainlink price feed was updated
* @return answeredInRound The round ID in which the answer was computed of the lowest updatedAt feed
*/
function latestRoundData()
public
view
returns (uint80, int256, uint256, uint256, uint80)
{
(
uint80 collateralToBridgeAssetRoundId,
int256 collateralToBridgeAssetPrice,
uint collateralToBridgeAssetStartedAt,
uint collateralToBridgeAssetUpdatedAt,
uint80 collateralToBridgeAssetAnsweredInRound
) = collateralToBridgeAsset.latestRoundData();
(
uint80 bridgeAssetToUsdRoundId,
int256 bridgeAssetToUsdPrice,
uint bridgeAssetToUsdStartedAt,
uint bridgeAssetToUsdUpdatedAt,
uint80 bridgeAssetToUsdAnsweredInRound
) = bridgeAssetToUsd.latestRoundData();
int price;
if(bridgeAssetDenominator){
price = bridgeAssetToUsdPrice * collateralToBridgeAssetPrice / 10 ** 18;
} else {
price = bridgeAssetToUsdPrice * 10 ** 18 / collateralToBridgeAssetPrice;
}
if (collateralToBridgeAssetUpdatedAt < bridgeAssetToUsdUpdatedAt) {
return (
collateralToBridgeAssetRoundId,
price,
collateralToBridgeAssetStartedAt,
collateralToBridgeAssetUpdatedAt,
collateralToBridgeAssetAnsweredInRound
);
} else {
return (
bridgeAssetToUsdRoundId,
price,
bridgeAssetToUsdStartedAt,
bridgeAssetToUsdUpdatedAt,
bridgeAssetToUsdAnsweredInRound
);
}
}
/**
* @notice Returns the latest price only
* @dev Unlike chainlink oracles, the latestAnswer will always be the same as in the latestRoundData
* @return int256 Returns the last finalized price of the chainlink oracle
*/
function latestAnswer() external view returns (int256) {
(, int256 latestPrice, , , ) = latestRoundData();
return latestPrice;
}
}
139 changes: 139 additions & 0 deletions test/feedForkTests/ChainlinkBridgeAssetBase.t.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.19;

import "forge-std/Test.sol";
import {console} from "forge-std/console.sol";
import "src/feeds/ChainlinkBasePriceFeed.sol";
import {ChainlinkBridgeAssetFeed} from "src/feeds/ChainlinkBridgeAssetFeed.sol";

abstract contract ChainlinkBridgeAssetBase is Test {
ChainlinkBridgeAssetFeed feed;
ChainlinkBasePriceFeed collateralToBridgeAssetFeed; // main coin1 feed
ChainlinkBasePriceFeed bridgeAssetToUsdFeed; // main coin2 feed

uint256 public constant SCALE = 1e18;

function init(
address _collateralToBridgeAssetFeed,
address _bridgeAssetToUsdFeed,
bool _bridgeAssetDenominator
) public {
collateralToBridgeAssetFeed = ChainlinkBasePriceFeed(_collateralToBridgeAssetFeed);
bridgeAssetToUsdFeed = ChainlinkBasePriceFeed(_bridgeAssetToUsdFeed);
feed = new ChainlinkBridgeAssetFeed(_collateralToBridgeAssetFeed, _bridgeAssetToUsdFeed, _bridgeAssetDenominator);
console.log("feed :", feed.description());
}

function test_decimals() public {
assertEq(feed.decimals(), 18);
}

function test_latestAnswer() public {
(, int256 lpUsdPrice, , , ) = feed.latestRoundData();

assertEq(feed.latestAnswer(), lpUsdPrice);
}

function test_collateralToBridgeAssetIncrease() public {
(,int256 priceBefore,,,) = feed.latestRoundData();

(uint80 roundId, int256 price, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
= collateralToBridgeAssetFeed.latestRoundData();

_mockLatestRoundData(
address(collateralToBridgeAssetFeed),
roundId,
price * 110 / 100,
startedAt,
updatedAt,
answeredInRound
);

(,int256 priceAfter,,,) = feed.latestRoundData();

if(feed.bridgeAssetDenominator()){
assertGt(priceAfter, priceBefore);
} else {
assertLt(priceAfter, priceBefore);
}
}

function test_collateralToBridgeAssetDecrease() public {
(,int256 priceBefore,,,) = feed.latestRoundData();

(uint80 roundId, int256 price, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
= collateralToBridgeAssetFeed.latestRoundData();

_mockLatestRoundData(
address(collateralToBridgeAssetFeed),
roundId,
price * 90 / 100,
startedAt,
updatedAt,
answeredInRound
);

(,int256 priceAfter,,,) = feed.latestRoundData();

if(feed.bridgeAssetDenominator()){
assertLt(priceAfter, priceBefore);
} else {
assertGt(priceAfter, priceBefore);
}
}

function test_bridgeAssetToUsdIncrease() public {
(,int256 priceBefore,,,) = feed.latestRoundData();

(uint80 roundId, int256 price, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
= collateralToBridgeAssetFeed.latestRoundData();

_mockLatestRoundData(
address(bridgeAssetToUsdFeed),
roundId,
price * 110 / 100,
startedAt,
updatedAt,
answeredInRound
);

(,int256 priceAfter,,,) = feed.latestRoundData();

assertLt(priceAfter, priceBefore);
}

function test_bridgeAssetToUsdDecrease() public {
(,int256 priceBefore,,,) = feed.latestRoundData();

(uint80 roundId, int256 price, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound)
= collateralToBridgeAssetFeed.latestRoundData();

_mockLatestRoundData(
address(bridgeAssetToUsdFeed),
roundId,
price * 90 / 100,
startedAt,
updatedAt,
answeredInRound
);

(,int256 priceAfter,,,) = feed.latestRoundData();

assertLt(priceAfter, priceBefore);
}

function _mockLatestRoundData(
address target,
uint80 roundId,
int256 price,
uint256 startedAt,
uint256 updatedAt,
uint80 answeredInRound
) internal {
vm.mockCall(
target,
abi.encodeWithSelector(IChainlinkFeed.latestRoundData.selector),
abi.encode(roundId, price, startedAt, updatedAt, answeredInRound)
);
}
}
Loading
Loading