diff --git a/contracts/src/PriceFeeds/FXPriceFeed.sol b/contracts/src/PriceFeeds/FXPriceFeed.sol index af615a8ae..7d8eb538e 100644 --- a/contracts/src/PriceFeeds/FXPriceFeed.sol +++ b/contracts/src/PriceFeeds/FXPriceFeed.sol @@ -35,6 +35,9 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @notice Whether the contract has been shutdown due to an oracle failure bool public isShutdown; + // @notice Whether the rate from the OracleAdapter should be inverted + bool public invertRateFeed; + /// @notice Thrown when the attempting to shutdown the contract when it is already shutdown error IsShutDown(); /// @notice Thrown when a non-watchdog address attempts to shutdown the contract @@ -47,6 +50,11 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /// @param _newRateFeedID The new rate feed ID event RateFeedIDUpdated(address indexed _oldRateFeedID, address indexed _newRateFeedID); + /// @notice Emitted when the invert rate feed flag is updated + /// @param _oldInvertRateFeed The previous invert rate feed flag + /// @param _newInvertRateFeed The new invert rate feed flag + event InvertRateFeedUpdated(bool _oldInvertRateFeed, bool _newInvertRateFeed); + /// @notice Emitted when the watchdog address is updated /// @param _oldWatchdogAddress The previous watchdog address /// @param _newWatchdogAddress The new watchdog address @@ -69,6 +77,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { * @notice Initializes the FXPriceFeed contract * @param _oracleAdapterAddress The address of the OracleAdapter contract * @param _rateFeedID The address of the rate feed ID + * @param _invertRateFeed Whether the rate from the OracleAdapter should be inverted * @param _borrowerOperationsAddress The address of the BorrowerOperations contract * @param _watchdogAddress The address of the watchdog contract * @param _initialOwner The address of the initial owner @@ -76,6 +85,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { function initialize( address _oracleAdapterAddress, address _rateFeedID, + bool _invertRateFeed, address _borrowerOperationsAddress, address _watchdogAddress, address _initialOwner @@ -88,6 +98,7 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { oracleAdapter = IOracleAdapter(_oracleAdapterAddress); rateFeedID = _rateFeedID; + invertRateFeed = _invertRateFeed; borrowerOperations = IBorrowerOperations(_borrowerOperationsAddress); watchdogAddress = _watchdogAddress; @@ -105,6 +116,17 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { emit RateFeedIDUpdated(oldRateFeedID, _newRateFeedID); } + /** + * @notice Sets the invert rate feed flag + * @param _invertRateFeed Whether the rate from the OracleAdapter should be inverted + */ + function setInvertRateFeed(bool _invertRateFeed) external onlyOwner { + bool oldInvertRateFeed = invertRateFeed; + invertRateFeed = _invertRateFeed; + + emit InvertRateFeedUpdated(oldInvertRateFeed, _invertRateFeed); + } + /** * @notice Sets the watchdog address * @param _newWatchdogAddress The address of the new watchdog contract @@ -121,15 +143,23 @@ contract FXPriceFeed is IPriceFeed, OwnableUpgradeable { /** * @notice Fetches the price of the FX rate, if valid * @dev If the contract is shutdown due to oracle failure, the last valid price is returned - * @return The price of the FX rate + * @return price The price of the FX rate */ - function fetchPrice() public returns (uint256) { + function fetchPrice() public returns (uint256 price) { if (isShutdown) { return lastValidPrice; } - // Denominator is always 1e18, so we only use the numerator as the price - (uint256 price,) = oracleAdapter.getFXRateIfValid(rateFeedID); + (uint256 numerator, uint256 denominator) = oracleAdapter.getFXRateIfValid(rateFeedID); + + if (invertRateFeed) { + // Multiply by 1e18 to get the price in 18 decimals + price = (denominator * 1e18) / numerator; + } else { + // Denominator is always 1e18, so we only use the numerator as the price + assert(denominator == 1e18); + price = numerator; + } lastValidPrice = price; diff --git a/contracts/test/FXPriceFeed.t.sol b/contracts/test/FXPriceFeed.t.sol index e3b0a0a11..0d9f95109 100644 --- a/contracts/test/FXPriceFeed.t.sol +++ b/contracts/test/FXPriceFeed.t.sol @@ -36,6 +36,7 @@ contract MockOracleAdapter { contract FXPriceFeedTest is Test { event WatchdogAddressUpdated(address indexed _oldWatchdogAddress, address indexed _newWatchdogAddress); + event InvertRateFeedUpdated(bool _oldInvertRateFeed, bool _newInvertRateFeed); event FXPriceFeedShutdown(); FXPriceFeed public fxPriceFeed; @@ -54,6 +55,7 @@ contract FXPriceFeedTest is Test { fxPriceFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -78,6 +80,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -91,6 +94,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(0), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -104,6 +108,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), address(0), + false, address(mockBorrowerOperations), watchdog, owner @@ -117,6 +122,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(0), watchdog, owner @@ -130,6 +136,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), address(0), owner @@ -143,6 +150,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, address(0) @@ -157,6 +165,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -176,6 +185,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -185,6 +195,7 @@ contract FXPriceFeedTest is Test { newFeed.initialize( address(mockOracleAdapter), rateFeedID, + false, address(mockBorrowerOperations), watchdog, owner @@ -218,6 +229,34 @@ contract FXPriceFeedTest is Test { assertEq(fxPriceFeed.rateFeedID(), newRateFeedID); } + function test_setInvertRateFeed_whenCalledByNonOwner_shouldRevert() initialized public { + address notOwner = makeAddr("notOwner"); + bool newInvertRateFeed = true; + + vm.prank(notOwner); + vm.expectRevert("Ownable: caller is not the owner"); + fxPriceFeed.setInvertRateFeed(newInvertRateFeed); + vm.stopPrank(); + } + + function test_setInvertRateFeed_whenCalledByOwner_shouldSucceed() initialized public { + vm.startPrank(owner); + vm.expectEmit(); + emit InvertRateFeedUpdated(false, true); + fxPriceFeed.setInvertRateFeed(true); + vm.stopPrank(); + + assertEq(fxPriceFeed.invertRateFeed(), true); + + vm.startPrank(owner); + vm.expectEmit(); + emit InvertRateFeedUpdated(true, false); + fxPriceFeed.setInvertRateFeed(false); + vm.stopPrank(); + + assertEq(fxPriceFeed.invertRateFeed(), false); + } + function test_setWatchdogAddress_whenCalledByNonOwner_shouldRevert() initialized public { address notOwner = makeAddr("notOwner"); address newWatchdog = makeAddr("newWatchdog"); @@ -270,6 +309,23 @@ contract FXPriceFeedTest is Test { assertEq(fxPriceFeed.lastValidPrice(), initialPrice); } + function test_fetchPrice_whenInvertRateFeedIsTrue_shouldReturnInvertedPrice() initialized public { + vm.startPrank(owner); + fxPriceFeed.setInvertRateFeed(true); + vm.stopPrank(); + + uint256 price = fxPriceFeed.fetchPrice(); + + assertEq(price, (mockRateDenominator * 1e18) / mockRateNumerator); + assertEq(fxPriceFeed.lastValidPrice(), (mockRateDenominator * 1e18) / mockRateNumerator); + + uint256 XOFUSDRateNumerator = 1771165426850867; // 0.001771 USD = ~1 XOF + mockOracleAdapter.setFXRate(XOFUSDRateNumerator, 1e18); + + assertEq(fxPriceFeed.fetchPrice(), 564600000000000277670); // 1 USD = ~564 XOF + assertEq(fxPriceFeed.lastValidPrice(), 564600000000000277670); + } + function test_shutdown_whenCalledByNonWatchdog_shouldRevert() initialized public { address notWatchdog = makeAddr("notWatchdog");