diff --git a/src/loan/lib/Chainlink.sol b/src/loan/lib/Chainlink.sol index 032fdb3..4114d9c 100644 --- a/src/loan/lib/Chainlink.sol +++ b/src/loan/lib/Chainlink.sol @@ -5,7 +5,6 @@ import { Math } from "openzeppelin/utils/math/Math.sol"; import { IChainlinkAggregatorLike } from "pwn/interfaces/IChainlinkAggregatorLike.sol"; import { IChainlinkFeedRegistryLike } from "pwn/interfaces/IChainlinkFeedRegistryLike.sol"; -import { ChainlinkDenominations } from "pwn/loan/lib/ChainlinkDenominations.sol"; library Chainlink { @@ -21,24 +20,24 @@ library Chainlink { uint256 public constant L2_GRACE_PERIOD = 10 minutes; /** - * @notice Throw when Chainlink feed returns negative price. + * @notice Chainlink address of ETH asset. */ - error ChainlinkFeedReturnedNegativePrice(address asset, address denominator, int256 price); + address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; /** - * @notice Throw when Chainlink feed for asset is not found. + * @notice Throw when Chainlink feed returns negative price. */ - error ChainlinkFeedNotFound(address asset); + error ChainlinkFeedReturnedNegativePrice(address feed, int256 price, uint256 updatedAt); /** - * @notice Throw when common denominator for credit and collateral assets is not found. + * @notice Throw when Chainlink feed price is too old. */ - error ChainlinkFeedCommonDenominatorNotFound(address creditAsset, address collateralAsset); + error ChainlinkFeedPriceTooOld(address feed, uint256 updatedAt); /** - * @notice Throw when Chainlink feed price is too old. + * @notice Throw when feed invert array is not exactly one item longer than intermediary feed array. */ - error ChainlinkFeedPriceTooOld(address asset, uint256 updatedAt); + error ChainlinkInvalidInputLenghts(); /** * @notice Throw when L2 Sequencer uptime feed returns that the sequencer is down. @@ -54,7 +53,7 @@ library Chainlink { /** * @notice Checks the uptime status of the L2 sequencer. * @dev This function reverts if the sequencer is down or if the grace period is not over. - * @param l2SequencerUptimeFeed The Chainlink aggregator contract that provides the sequencer uptime status. + * @param l2SequencerUptimeFeed The Chainlink feed that provides the sequencer uptime status. */ function checkSequencerUptime(IChainlinkAggregatorLike l2SequencerUptimeFeed) internal view { if (address(l2SequencerUptimeFeed) != address(0)) { @@ -73,194 +72,139 @@ library Chainlink { } /** - * @notice Fetches the prices of the credit and collateral assets with a common denominator. - * @dev This function ensures that the prices of both assets are converted to the same denominator for comparison. + * @notice Fetches the prices of the credit with collateral assets as denomination. + * @dev `feedInvertFlags` array must be exactly one item longer than `feedIntermediaryDenominations`. * @param feedRegistry The Chainlink feed registry contract that provides the price feeds. * @param creditAsset The address of the credit asset. * @param collateralAsset The address of the collateral asset. - * @return The prices of the credit assets with a common denominator. - * @return The prices of the collateral assets with a common denominator. + * @param feedIntermediaryDenominations List of intermediary price feeds that will be fetched to get to the collateral asset denominator. + * @param feedInvertFlags List of flags indicating if price feeds exist only for inverted base and quote assets. + * @return The price of the credit assets denominated in collateral assets. + * @return The price decimals. */ - function fetchPricesWithCommonDenominator( + function fetchCreditPriceWithCollateralDenomination( IChainlinkFeedRegistryLike feedRegistry, address creditAsset, - address collateralAsset - ) internal view returns (uint256, uint256) { - // fetch asset prices - (uint256 creditPrice, uint8 creditPriceDecimals, address creditDenominator) - = findPrice(feedRegistry, creditAsset); - (uint256 collateralPrice, uint8 collateralPriceDecimals, address collateralDenominator) - = findPrice(feedRegistry, collateralAsset); - - // convert prices to the same denominator - // Note: assume only USD, ETH, or BTC can be denominators - if (creditDenominator != collateralDenominator) { - - // We can assume that most assets have price feed in USD. If not, we need to find common denominator. - // Table below shows conversions between assets. - // ------------------------- - // | | USD | ETH | BTC | <-- credit - // | USD | X | ETH | BTC | - // | ETH | ETH | X | ETH | - // | BTC | BTC | ETH | X | - // ------------------------- - // ^ collateral - // - // For this to work, we need to have this price feeds: ETH/USD, ETH/BTC, BTC/USD. - // This will cover most of the cases, where assets don't have price feed in USD. - - bool success = true; - if (creditDenominator == ChainlinkDenominations.USD) { - (success, creditPrice, creditPriceDecimals) = convertPriceDenominator({ - feedRegistry: feedRegistry, - nominatorPrice: creditPrice, - nominatorDecimals: creditPriceDecimals, - originalDenominator: creditDenominator, - newDenominator: collateralDenominator - }); - } else { - (success, collateralPrice, collateralPriceDecimals) = convertPriceDenominator({ - feedRegistry: feedRegistry, - nominatorPrice: collateralPrice, - nominatorDecimals: collateralPriceDecimals, - originalDenominator: collateralDenominator, - newDenominator: collateralDenominator == ChainlinkDenominations.USD - ? creditDenominator - : ChainlinkDenominations.ETH - }); - } - - if (!success) { - revert ChainlinkFeedCommonDenominatorNotFound({ - creditAsset: creditAsset, - collateralAsset: collateralAsset - }); - } + address collateralAsset, + address[] memory feedIntermediaryDenominations, + bool[] memory feedInvertFlags + ) internal view returns (uint256, uint8) { + if (feedInvertFlags.length != feedIntermediaryDenominations.length + 1) { + revert ChainlinkInvalidInputLenghts(); } - // scale prices to the higher decimals - if (creditPriceDecimals > collateralPriceDecimals) { - collateralPrice = scalePrice(collateralPrice, collateralPriceDecimals, creditPriceDecimals); - } else if (creditPriceDecimals < collateralPriceDecimals) { - creditPrice = scalePrice(creditPrice, creditPriceDecimals, collateralPriceDecimals); + // initial state + uint256 price = 1; + uint8 priceDecimals = 0; + + // iterate until collateral asset is denominator + for (uint256 i; i < feedInvertFlags.length; ++i) { + (price, priceDecimals) = convertPriceDenomination({ + feedRegistry: feedRegistry, + currentPrice: price, + currentDecimals: priceDecimals, + currentDenomination: i == 0 ? creditAsset : feedIntermediaryDenominations[i - 1], + nextDenomination: i == feedIntermediaryDenominations.length ? collateralAsset : feedIntermediaryDenominations[i], + nextInvert: feedInvertFlags[i] + }); } - return (creditPrice, collateralPrice); + return (price, priceDecimals); } /** - * @notice Find price for an asset in USD, ETH, or BTC denominator. - * @param asset Address of an asset. - * @return price Price of an asset. - * @return priceDecimals Decimals of the price. - * @return denominator Address of a denominator asset. + * @notice Convert price denomination. + * @param feedRegistry The Chainlink feed registry contract that provides the price feeds. + * @param currentPrice Price of an asset denominated in `currentDenomination`. + * @param currentDecimals Decimals of the current price. + * @param currentDenomination Address of the current denomination. + * @param nextDenomination Address of the denomination to convert the current price to. + * @param nextInvert Flag, if intermediary price feed exists only with inverted base and quote assets. + * @return nextPrice Price of an asset denomination in `nextDenomination`. + * @return nextDecimals Decimals of the next price. */ - function findPrice(IChainlinkFeedRegistryLike feedRegistry, address asset) - internal - view - returns (uint256, uint8, address) - { - // fetch USD denominated price - (bool success, uint256 price, uint8 priceDecimals) = fetchPrice(feedRegistry, asset, ChainlinkDenominations.USD); - if (success) { - return (price, priceDecimals, ChainlinkDenominations.USD); - } + function convertPriceDenomination( + IChainlinkFeedRegistryLike feedRegistry, + uint256 currentPrice, + uint8 currentDecimals, + address currentDenomination, + address nextDenomination, + bool nextInvert + ) internal view returns (uint256 nextPrice, uint8 nextDecimals) { + // fetch convert price + (uint256 intermediaryPrice, uint8 intermediaryDecimals) = fetchPrice({ + feedRegistry: feedRegistry, + asset: nextInvert ? nextDenomination : currentDenomination, + denomination: nextInvert ? currentDenomination : nextDenomination + }); - // fetch ETH denominated price - (success, price, priceDecimals) = fetchPrice(feedRegistry, asset, ChainlinkDenominations.ETH); - if (success) { - return (price, priceDecimals, ChainlinkDenominations.ETH); - } + // sync decimals + (currentPrice, intermediaryPrice, nextDecimals) + = syncDecimalsUp(currentPrice, currentDecimals, intermediaryPrice, intermediaryDecimals); - // fetch BTC denominated price - (success, price, priceDecimals) = fetchPrice(feedRegistry, asset, ChainlinkDenominations.BTC); - if (success) { - return (price, priceDecimals, ChainlinkDenominations.BTC); + // compute price with new denomination + if (nextInvert) { + nextPrice = Math.mulDiv(currentPrice, 10 ** nextDecimals, intermediaryPrice); + } else { + nextPrice = Math.mulDiv(currentPrice, intermediaryPrice, 10 ** nextDecimals); } - // revert if asset doesn't have price denominated in USD, ETH, or BTC - revert ChainlinkFeedNotFound({ asset: asset }); + return (nextPrice, nextDecimals); } /** * @notice Fetch price from Chainlink feed. + * @param feedRegistry The Chainlink feed registry contract that provides the price feeds. * @param asset Address of an asset. - * @param denominator Address of a denominator asset. - * @return success True if price was fetched successfully. + * @param denomination Address of a denomination asset. * @return price Price of an asset. * @return decimals Decimals of a price. */ - function fetchPrice(IChainlinkFeedRegistryLike feedRegistry, address asset, address denominator) + function fetchPrice(IChainlinkFeedRegistryLike feedRegistry, address asset, address denomination) internal view - returns (bool, uint256, uint8) + returns (uint256, uint8) { - try feedRegistry.getFeed(asset, denominator) returns (IChainlinkAggregatorLike aggregator) { - (, int256 price,, uint256 updatedAt,) = aggregator.latestRoundData(); - if (price < 0) { - revert ChainlinkFeedReturnedNegativePrice({ asset: asset, denominator: denominator, price: price }); - } - if (block.timestamp - updatedAt > MAX_CHAINLINK_FEED_PRICE_AGE) { - revert ChainlinkFeedPriceTooOld({ asset: asset, updatedAt: updatedAt }); - } + IChainlinkAggregatorLike feed = feedRegistry.getFeed(asset, denomination); - uint8 decimals = aggregator.decimals(); - return (true, uint256(price), decimals); - } catch { - return (false, 0, 0); - } - } + // Note: registry reverts with "Feed not found" for no registered feed - /** - * @notice Convert price denominator. - * @param nominatorPrice Price of an asset denomination in `originalDenominator`. - * @param nominatorDecimals Decimals of a price in `originalDenominator`. - * @param originalDenominator Address of an original denominator asset. - * @param newDenominator Address of a new denominator asset. - * @return success True if conversion was successful. - * @return nominatorPrice Price of an asset denomination in `newDenominator`. - * @return nominatorDecimals Decimals of a price in `newDenominator`. - */ - function convertPriceDenominator( - IChainlinkFeedRegistryLike feedRegistry, - uint256 nominatorPrice, - uint8 nominatorDecimals, - address originalDenominator, - address newDenominator - ) internal view returns (bool, uint256, uint8) { - (bool success, uint256 price, uint8 priceDecimals) = fetchPrice({ - feedRegistry: feedRegistry, asset: newDenominator, denominator: originalDenominator - }); - - if (!success) { - return (false, nominatorPrice, nominatorDecimals); + (, int256 price,, uint256 updatedAt,) = feed.latestRoundData(); + if (price < 0) { + revert ChainlinkFeedReturnedNegativePrice({ feed: address(feed), price: price, updatedAt: updatedAt }); } - - if (priceDecimals < nominatorDecimals) { - price = scalePrice(price, priceDecimals, nominatorDecimals); - } else if (priceDecimals > nominatorDecimals) { - nominatorPrice = scalePrice(nominatorPrice, nominatorDecimals, priceDecimals); - nominatorDecimals = priceDecimals; + if (block.timestamp - updatedAt > MAX_CHAINLINK_FEED_PRICE_AGE) { + revert ChainlinkFeedPriceTooOld({ feed: address(feed), updatedAt: updatedAt }); } - nominatorPrice = Math.mulDiv(nominatorPrice, 10 ** nominatorDecimals, price); - return (true, nominatorPrice, nominatorDecimals); + return (uint256(price), feed.decimals()); } /** - * @notice Scale price to new decimals. - * @param price Price to be scaled. - * @param priceDecimals Decimals of a price. - * @param newDecimals New decimals. - * @return Scaled price. + * @notice Sync price decimals to the higher one. + * @param price1 Price one to be scaled. + * @param decimals1 Decimals of the price one. + * @param price2 Price two to be scaled. + * @param decimals2 Decimals of the price two. + * @return Synced price one. + * @return Synced price two. + * @return Synced price decimals. */ - function scalePrice(uint256 price, uint8 priceDecimals, uint8 newDecimals) internal pure returns (uint256) { - if (priceDecimals < newDecimals) { - return price * 10 ** (newDecimals - priceDecimals); - } else if (priceDecimals > newDecimals) { - return price / 10 ** (priceDecimals - newDecimals); + function syncDecimalsUp(uint256 price1, uint8 decimals1, uint256 price2, uint8 decimals2) + internal + pure + returns (uint256, uint256, uint8) + { + uint8 syncedDecimals; + if (decimals1 > decimals2) { + syncedDecimals = decimals1; + price2 *= 10 ** (decimals1 - decimals2); + } else { + syncedDecimals = decimals2; + price1 *= 10 ** (decimals2 - decimals1); } - return price; + + return (price1, price2, syncedDecimals); } } diff --git a/src/loan/lib/ChainlinkDenominations.sol b/src/loan/lib/ChainlinkDenominations.sol deleted file mode 100644 index 4dc5a93..0000000 --- a/src/loan/lib/ChainlinkDenominations.sol +++ /dev/null @@ -1,30 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity 0.8.16; - -/** - * @dev Copy from https://github.com/smartcontractkit/chainlink/blob/d14a9b5111baaffa95266b0d39ea21f6ecfd0137/contracts/src/v0.8/Denominations.sol - */ -library ChainlinkDenominations { - address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; - address public constant BTC = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; - - // Fiat currencies follow https://en.wikipedia.org/wiki/ISO_4217 - address public constant USD = address(840); - address public constant GBP = address(826); - address public constant EUR = address(978); - address public constant JPY = address(392); - address public constant KRW = address(410); - address public constant CNY = address(156); - address public constant AUD = address(36); - address public constant CAD = address(124); - address public constant CHF = address(756); - address public constant ARS = address(32); - address public constant PHP = address(608); - address public constant NZD = address(554); - address public constant SGD = address(702); - address public constant NGN = address(566); - address public constant ZAR = address(710); - address public constant RUB = address(643); - address public constant INR = address(356); - address public constant BRL = address(986); -} diff --git a/src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol b/src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol index 12ffe89..94b8744 100644 --- a/src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol +++ b/src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol @@ -7,7 +7,6 @@ import { Math } from "openzeppelin/utils/math/Math.sol"; import { Chainlink, - ChainlinkDenominations, IChainlinkFeedRegistryLike, IChainlinkAggregatorLike } from "pwn/loan/lib/Chainlink.sol"; @@ -28,6 +27,8 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { string public constant VERSION = "1.0"; + uint256 public constant MAX_INTERMEDIARY_DENOMINATIONS = 2; + /** * @notice Loan to value denominator. It is used to calculate collateral amount from credit amount. */ @@ -37,7 +38,7 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { * @dev EIP-712 simple proposal struct type hash. */ bytes32 public constant PROPOSAL_TYPEHASH = keccak256( - "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 loanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 fixedInterestAmount,uint24 accruingInterestAPR,uint32 durationOrDate,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" + "Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 loanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 fixedInterestAmount,uint24 accruingInterestAPR,uint32 durationOrDate,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)" ); /** @@ -48,6 +49,8 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { * @param checkCollateralStateFingerprint If true, the collateral state fingerprint will be checked during proposal acceptance. * @param collateralStateFingerprint Fingerprint of a collateral state. It is used to check if a collateral is in a valid state. * @param creditAddress Address of an asset which is lended to a borrower. + * @param feedIntermediaryDenominations List of intermediary price feeds that will be fetched to get to the collateral asset denominator. + * @param feedInvertFlags List of flags indicating if price feeds exist only for inverted base and quote assets. * @param loanToValue Loan to value ratio with 4 decimals. E.g., 6231 == 0.6231 == 62.31%. * @param minCreditAmount Minimum amount of tokens which can be borrowed using the proposal. * @param availableCreditLimit Available credit limit for the proposal. It is the maximum amount of tokens which can be borrowed using the proposal. If non-zero, proposal can be accepted more than once, until the credit limit is reached. @@ -72,6 +75,8 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { bool checkCollateralStateFingerprint; bytes32 collateralStateFingerprint; address creditAddress; + address[] feedIntermediaryDenominations; + bool[] feedInvertFlags; uint256 loanToValue; uint256 minCreditAmount; uint256 availableCreditLimit; @@ -126,10 +131,15 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { error MinCreditAmountNotSet(); /** - * @notice Throw when proposal credit amount is insufficient. + * @notice Thrown when proposal credit amount is insufficient. */ error InsufficientCreditAmount(uint256 current, uint256 limit); + /** + * @notice Thrown when intermediary denominations are out of bounds. + */ + error IntermediaryDenominationsOutOfBounds(uint256 current, uint256 limit); + constructor( address _hub, @@ -195,32 +205,55 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { * @param creditAddress Address of credit token. * @param creditAmount Amount of credit. * @param collateralAddress Address of collateral token. + * @param feedIntermediaryDenominations List of intermediary price feeds that will be fetched to get to the collateral asset denominator. + * @param feedInvertFlags List of flags indicating if price feeds exist only for inverted base and quote assets. * @param loanToValue Loan to value ratio with 4 decimals. E.g., 6231 == 0.6231 == 62.31%. * @return Amount of collateral. */ function getCollateralAmount( - address creditAddress, uint256 creditAmount, address collateralAddress, uint256 loanToValue + address creditAddress, + uint256 creditAmount, + address collateralAddress, + address[] memory feedIntermediaryDenominations, + bool[] memory feedInvertFlags, + uint256 loanToValue ) public view returns (uint256) { // check L2 sequencer uptime if necessary l2SequencerUptimeFeed.checkSequencerUptime(); - // fetch asset prices + // don't allow more than 2 intermediary denominations + if (feedIntermediaryDenominations.length > MAX_INTERMEDIARY_DENOMINATIONS) { + revert IntermediaryDenominationsOutOfBounds({ + current: feedIntermediaryDenominations.length, + limit: MAX_INTERMEDIARY_DENOMINATIONS + }); + } + + // fetch credit asset price with collateral asset as denomination // Note: use ETH price feed for WETH asset due to absence of WETH price feed - (uint256 creditPrice, uint256 collateralPrice) = chainlinkFeedRegistry.fetchPricesWithCommonDenominator({ - creditAsset: creditAddress == WETH ? ChainlinkDenominations.ETH : creditAddress, - collateralAsset: collateralAddress == WETH ? ChainlinkDenominations.ETH : collateralAddress + (uint256 price, uint8 priceDecimals) = chainlinkFeedRegistry.fetchCreditPriceWithCollateralDenomination({ + creditAsset: creditAddress == WETH ? Chainlink.ETH : creditAddress, + collateralAsset: collateralAddress == WETH ? Chainlink.ETH : collateralAddress, + feedIntermediaryDenominations: feedIntermediaryDenominations, + feedInvertFlags: feedInvertFlags }); // fetch asset decimals uint256 creditDecimals = safeFetchDecimals(creditAddress); uint256 collateralDecimals = safeFetchDecimals(collateralAddress); - // calculate collateral amount - return Math.mulDiv( - creditAmount * 10 ** (collateralDecimals > creditDecimals ? collateralDecimals - creditDecimals : 0), - creditPrice * LOAN_TO_VALUE_DENOMINATOR, - collateralPrice * loanToValue - ) / 10 ** (collateralDecimals < creditDecimals ? creditDecimals - collateralDecimals : 0); + if (collateralDecimals > creditDecimals) { + creditAmount *= 10 ** (collateralDecimals - creditDecimals); + } + + uint256 collateralAmount = Math.mulDiv(creditAmount, price, 10 ** priceDecimals); + collateralAmount = Math.mulDiv(collateralAmount, LOAN_TO_VALUE_DENOMINATOR, loanToValue); + + if (collateralDecimals < creditDecimals) { + collateralAmount /= 10 ** (creditDecimals - collateralDecimals); + } + + return collateralAmount; } /** @@ -254,6 +287,8 @@ contract PWNSimpleLoanElasticChainlinkProposal is PWNSimpleLoanProposal { proposal.creditAddress, proposalValues.creditAmount, proposal.collateralAddress, + proposal.feedIntermediaryDenominations, + proposal.feedInvertFlags, proposal.loanToValue ); diff --git a/test/fork/PWNSimpleLoanElasticChainlinkProposal.fork.t.sol b/test/fork/PWNSimpleLoanElasticChainlinkProposal.fork.t.sol index ec9381e..dfc525a 100644 --- a/test/fork/PWNSimpleLoanElasticChainlinkProposal.fork.t.sol +++ b/test/fork/PWNSimpleLoanElasticChainlinkProposal.fork.t.sol @@ -5,10 +5,10 @@ import { MultiToken, IERC20 } from "MultiToken/MultiToken.sol"; import { IChainlinkAggregatorLike, - IChainlinkFeedRegistryLike, - ChainlinkDenominations + IChainlinkFeedRegistryLike } from "src/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol"; +import { ChainlinkDenominations } from "test/helper/ChainlinkDenominations.sol"; import { IPWNDeployer, PWNHub, @@ -28,7 +28,94 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { } - function test_USDT_WETH() external { + function test_oneFeed_APE_WETH() external { + IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); + IERC20 APE = IERC20(0x4d224452801ACEd8B2F0aebE155379bb5D594381); + address APE_ETH_Feed = 0xc7de7f4d4C9c991fF62a07D18b3E31e349833A18; + + deal(lender, 10000 ether); + deal(borrower, 10000 ether); + deal(address(WETH), borrower, 1e18, false); + deal(address(APE), lender, 1000e18, false); + + // Register APE/ETH feed + vm.startPrank(deployment.protocolTimelock); + deployment.chainlinkFeedRegistry.proposeFeed(address(APE), ChainlinkDenominations.ETH, APE_ETH_Feed); + deployment.chainlinkFeedRegistry.confirmFeed(address(APE), ChainlinkDenominations.ETH, APE_ETH_Feed); + vm.stopPrank(); + + address[] memory feedIntermediaryDenominations = new address[](0); + bool[] memory feedInvertFlags = new bool[](1); + feedInvertFlags[0] = false; + + PWNSimpleLoanElasticChainlinkProposal.Proposal memory proposal = PWNSimpleLoanElasticChainlinkProposal.Proposal({ + collateralCategory: MultiToken.Category.ERC20, + collateralAddress: address(WETH), + collateralId: 0, + checkCollateralStateFingerprint: false, + collateralStateFingerprint: bytes32(0), + creditAddress: address(APE), + feedIntermediaryDenominations: feedIntermediaryDenominations, + feedInvertFlags: feedInvertFlags, + loanToValue: 8000, + minCreditAmount: 1, + availableCreditLimit: 1000e18, + utilizedCreditId: 0, + fixedInterestAmount: 0, + accruingInterestAPR: 0, + durationOrDate: 1 days, + expiration: uint40(block.timestamp + 7 days), + allowedAcceptor: address(0), + proposer: lender, + proposerSpecHash: deployment.simpleLoan.getLenderSpecHash(PWNSimpleLoan.LenderSpec(lender)), + isOffer: true, + refinancingLoanId: 0, + nonceSpace: 0, + nonce: 0, + loanContract: address(deployment.simpleLoan) + }); + + PWNSimpleLoanElasticChainlinkProposal.ProposalValues memory values = PWNSimpleLoanElasticChainlinkProposal.ProposalValues({ + creditAmount: 300e18 + }); + + vm.prank(borrower); + WETH.approve(address(deployment.simpleLoan), type(uint256).max); + vm.prank(lender); + APE.approve(address(deployment.simpleLoan), type(uint256).max); + + bytes memory signature = _sign(lenderPK, deployment.simpleLoanElasticChainlinkProposal.getProposalHash(proposal)); + bytes memory proposalData = deployment.simpleLoanElasticChainlinkProposal.encodeProposalData(proposal, values); + + vm.prank(borrower); + deployment.simpleLoan.createLOAN({ + proposalSpec: PWNSimpleLoan.ProposalSpec({ + proposalContract: address(deployment.simpleLoanElasticChainlinkProposal), + proposalData: proposalData, + proposalInclusionProof: new bytes32[](0), + signature: signature + }), + lenderSpec: PWNSimpleLoan.LenderSpec({ + sourceOfFunds: lender + }), + callerSpec: PWNSimpleLoan.CallerSpec({ + refinancingLoanId: 0, + revokeNonce: false, + nonce: 0 + }), + extra: "" + }); + + (, int256 price,,,) = IChainlinkAggregatorLike(APE_ETH_Feed).latestRoundData(); + uint256 expectedCollAmount = 300 * uint256(price) / 8 * 10; + + assertEq(APE.balanceOf(lender), 700e18); + assertEq(APE.balanceOf(borrower), 300e18); + assertApproxEqAbs(WETH.balanceOf(borrower), 1e18 - expectedCollAmount, 0.00001e18); + assertApproxEqAbs(WETH.balanceOf(address(deployment.simpleLoan)), expectedCollAmount, 0.00001e18); + } + + function test_twoFeeds_USDT_WETH() external { IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); address ETH_USD_Feed = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; @@ -47,6 +134,12 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { deployment.chainlinkFeedRegistry.confirmFeed(ChainlinkDenominations.ETH, ChainlinkDenominations.USD, ETH_USD_Feed); vm.stopPrank(); + address[] memory feedIntermediaryDenominations = new address[](1); + feedIntermediaryDenominations[0] = ChainlinkDenominations.USD; + bool[] memory feedInvertFlags = new bool[](2); + feedInvertFlags[0] = false; + feedInvertFlags[1] = true; + PWNSimpleLoanElasticChainlinkProposal.Proposal memory proposal = PWNSimpleLoanElasticChainlinkProposal.Proposal({ collateralCategory: MultiToken.Category.ERC20, collateralAddress: address(WETH), @@ -54,6 +147,8 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { checkCollateralStateFingerprint: false, collateralStateFingerprint: bytes32(0), creditAddress: address(USDT), + feedIntermediaryDenominations: feedIntermediaryDenominations, + feedInvertFlags: feedInvertFlags, loanToValue: 8000, minCreditAmount: 1, availableCreditLimit: 1000e6, @@ -109,16 +204,15 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { (, int256 usdtPrice,,,) = IChainlinkAggregatorLike(USDT_USD_Feed).latestRoundData(); (, int256 ethPrice,,,) = IChainlinkAggregatorLike(ETH_USD_Feed).latestRoundData(); - // 625e18 = magic value when using credit valued at 500 USD with 80% LTV - uint256 expectedCollAmount = 625e18 * uint256(usdtPrice) / uint256(ethPrice); + uint256 expectedCollAmount = 500e18 * uint256(usdtPrice) / uint256(ethPrice) / 8 * 10; assertEq(USDT.balanceOf(lender), 500e6); assertEq(USDT.balanceOf(borrower), 500e6); - assertEq(WETH.balanceOf(borrower), 1e18 - expectedCollAmount); - assertEq(WETH.balanceOf(address(deployment.simpleLoan)), expectedCollAmount); + assertApproxEqAbs(WETH.balanceOf(borrower), 1e18 - expectedCollAmount, 0.00001e18); + assertApproxEqAbs(WETH.balanceOf(address(deployment.simpleLoan)), expectedCollAmount, 0.00001e18); } - function test_ARB_WETH() external { + function test_twoFeeds_ARB_WETH() external { IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 ARB = IERC20(0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1); address ETH_USD_Feed = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; @@ -137,6 +231,12 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { deployment.chainlinkFeedRegistry.confirmFeed(ChainlinkDenominations.ETH, ChainlinkDenominations.USD, ETH_USD_Feed); vm.stopPrank(); + address[] memory feedIntermediaryDenominations = new address[](1); + feedIntermediaryDenominations[0] = ChainlinkDenominations.USD; + bool[] memory feedInvertFlags = new bool[](2); + feedInvertFlags[0] = false; + feedInvertFlags[1] = true; + PWNSimpleLoanElasticChainlinkProposal.Proposal memory proposal = PWNSimpleLoanElasticChainlinkProposal.Proposal({ collateralCategory: MultiToken.Category.ERC20, collateralAddress: address(WETH), @@ -144,6 +244,8 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { checkCollateralStateFingerprint: false, collateralStateFingerprint: bytes32(0), creditAddress: address(ARB), + feedIntermediaryDenominations: feedIntermediaryDenominations, + feedInvertFlags: feedInvertFlags, loanToValue: 8000, minCreditAmount: 1, availableCreditLimit: 1000e18, @@ -196,16 +298,15 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { (, int256 arbPrice,,,) = IChainlinkAggregatorLike(ARB_USD_Feed).latestRoundData(); (, int256 ethPrice,,,) = IChainlinkAggregatorLike(ETH_USD_Feed).latestRoundData(); - // 625e18 = magic value when using credit valued at 500 USD with 80% LTV - uint256 expectedCollAmount = 625e18 * uint256(arbPrice) / uint256(ethPrice); + uint256 expectedCollAmount = 500e18 * uint256(arbPrice) / uint256(ethPrice) / 8 * 10; assertEq(ARB.balanceOf(lender), 500e18); assertEq(ARB.balanceOf(borrower), 500e18); - assertEq(WETH.balanceOf(borrower), 1e18 - expectedCollAmount); - assertEq(WETH.balanceOf(address(deployment.simpleLoan)), expectedCollAmount); + assertApproxEqAbs(WETH.balanceOf(borrower), 1e18 - expectedCollAmount, 0.00001e18); + assertApproxEqAbs(WETH.balanceOf(address(deployment.simpleLoan)), expectedCollAmount, 0.00001e18); } - function test_USDT_ARB() external { + function test_twoFeeds_USDT_ARB() external { IERC20 ARB = IERC20(0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1); IERC20 USDT = IERC20(0xdAC17F958D2ee523a2206206994597C13D831ec7); address ARB_USD_Feed = 0x31697852a68433DbCc2Ff612c516d69E3D9bd08F; @@ -224,6 +325,12 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { deployment.chainlinkFeedRegistry.confirmFeed(address(USDT), ChainlinkDenominations.USD, USDT_USD_Feed); vm.stopPrank(); + address[] memory feedIntermediaryDenominations = new address[](1); + feedIntermediaryDenominations[0] = ChainlinkDenominations.USD; + bool[] memory feedInvertFlags = new bool[](2); + feedInvertFlags[0] = false; + feedInvertFlags[1] = true; + PWNSimpleLoanElasticChainlinkProposal.Proposal memory proposal = PWNSimpleLoanElasticChainlinkProposal.Proposal({ collateralCategory: MultiToken.Category.ERC20, collateralAddress: address(ARB), @@ -231,6 +338,8 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { checkCollateralStateFingerprint: false, collateralStateFingerprint: bytes32(0), creditAddress: address(USDT), + feedIntermediaryDenominations: feedIntermediaryDenominations, + feedInvertFlags: feedInvertFlags, loanToValue: 8000, minCreditAmount: 1, availableCreditLimit: 1000e6, @@ -286,37 +395,39 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { (, int256 usdtPrice,,,) = IChainlinkAggregatorLike(USDT_USD_Feed).latestRoundData(); (, int256 arbPrice,,,) = IChainlinkAggregatorLike(ARB_USD_Feed).latestRoundData(); - // 625e18 = magic value when using credit valued at 500 USD with 80% LTV - uint256 expectedCollAmount = 625e18 * uint256(usdtPrice) / uint256(arbPrice); + uint256 expectedCollAmount = 500e18 * uint256(usdtPrice) / uint256(arbPrice) / 8 * 10; assertEq(USDT.balanceOf(lender), 500e6); assertEq(USDT.balanceOf(borrower), 500e6); - assertEq(ARB.balanceOf(borrower), 2000e18 - expectedCollAmount); - assertEq(ARB.balanceOf(address(deployment.simpleLoan)), expectedCollAmount); + assertApproxEqAbs(ARB.balanceOf(borrower), 2000e18 - expectedCollAmount, 0.00001e18); + assertApproxEqAbs(ARB.balanceOf(address(deployment.simpleLoan)), expectedCollAmount, 0.00001e18); } - function test_WETH_WBTC() external { + function test_twoFeeds_WETH_WBTC() external { IERC20 WETH = IERC20(0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2); IERC20 WBTC = IERC20(0x2260FAC5E5542a773Aa44fBCfeDf7C193bc2C599); - address ETH_USD_Feed = 0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419; address WBTC_BTC_Feed = 0xfdFD9C85aD200c506Cf9e21F1FD8dd01932FBB23; - address BTC_USD_Feed = 0xF4030086522a5bEEa4988F8cA5B36dbC97BeE88c; + address BTC_ETH_Feed = 0xdeb288F737066589598e9214E782fa5A8eD689e8; deal(lender, 10000 ether); deal(borrower, 10000 ether); deal(address(WBTC), borrower, 50e8, false); deal(address(WETH), lender, 1000e18, false); - // Register ARB/USD & ETH/USD feed + // Register WBTC/BTC, & BTC/ETH feed vm.startPrank(deployment.protocolTimelock); - deployment.chainlinkFeedRegistry.proposeFeed(ChainlinkDenominations.ETH, ChainlinkDenominations.USD, ETH_USD_Feed); - deployment.chainlinkFeedRegistry.confirmFeed(ChainlinkDenominations.ETH, ChainlinkDenominations.USD, ETH_USD_Feed); deployment.chainlinkFeedRegistry.proposeFeed(address(WBTC), ChainlinkDenominations.BTC, WBTC_BTC_Feed); deployment.chainlinkFeedRegistry.confirmFeed(address(WBTC), ChainlinkDenominations.BTC, WBTC_BTC_Feed); - deployment.chainlinkFeedRegistry.proposeFeed(ChainlinkDenominations.BTC, ChainlinkDenominations.USD, BTC_USD_Feed); - deployment.chainlinkFeedRegistry.confirmFeed(ChainlinkDenominations.BTC, ChainlinkDenominations.USD, BTC_USD_Feed); + deployment.chainlinkFeedRegistry.proposeFeed(ChainlinkDenominations.BTC, ChainlinkDenominations.ETH, BTC_ETH_Feed); + deployment.chainlinkFeedRegistry.confirmFeed(ChainlinkDenominations.BTC, ChainlinkDenominations.ETH, BTC_ETH_Feed); vm.stopPrank(); + address[] memory feedIntermediaryDenominations = new address[](1); + feedIntermediaryDenominations[0] = ChainlinkDenominations.BTC; + bool[] memory feedInvertFlags = new bool[](2); + feedInvertFlags[0] = true; + feedInvertFlags[1] = true; + PWNSimpleLoanElasticChainlinkProposal.Proposal memory proposal = PWNSimpleLoanElasticChainlinkProposal.Proposal({ collateralCategory: MultiToken.Category.ERC20, collateralAddress: address(WBTC), @@ -324,6 +435,8 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { checkCollateralStateFingerprint: false, collateralStateFingerprint: bytes32(0), creditAddress: address(WETH), + feedIntermediaryDenominations: feedIntermediaryDenominations, + feedInvertFlags: feedInvertFlags, loanToValue: 8000, minCreditAmount: 1, availableCreditLimit: 1000e18, @@ -374,16 +487,15 @@ contract PWNSimpleLoanElasticChainlinkProposalForkTest is DeploymentTest { }); - (, int256 ethPrice,,,) = IChainlinkAggregatorLike(ETH_USD_Feed).latestRoundData(); (, int256 wbtcPrice,,,) = IChainlinkAggregatorLike(WBTC_BTC_Feed).latestRoundData(); - (, int256 btcPrice,,,) = IChainlinkAggregatorLike(BTC_USD_Feed).latestRoundData(); - // 625 = magic value when using 500 credit tokens with 80% LTV - uint256 expectedCollAmount = 625e16 * uint256(ethPrice) / uint256(btcPrice) / uint256(wbtcPrice); + (, int256 btcPrice,,,) = IChainlinkAggregatorLike(BTC_ETH_Feed).latestRoundData(); + uint256 expectedCollAmount = 500e8 * 1e18 / uint256(btcPrice) * 1e8 / uint256(wbtcPrice) / 8 * 10; + assertEq(WETH.balanceOf(lender), 500e18); assertEq(WETH.balanceOf(borrower), 500e18); - assertApproxEqAbs(WBTC.balanceOf(borrower), 50e8 - expectedCollAmount, 0.0001e8); - assertApproxEqAbs(WBTC.balanceOf(address(deployment.simpleLoan)), expectedCollAmount, 0.0001e8); + assertApproxEqAbs(WBTC.balanceOf(borrower), 50e8 - expectedCollAmount, 0.00001e8); + assertApproxEqAbs(WBTC.balanceOf(address(deployment.simpleLoan)), expectedCollAmount, 0.00001e8); } } diff --git a/test/harness/ChainlinkHarness.sol b/test/harness/ChainlinkHarness.sol index ae08676..cbd0ad5 100644 --- a/test/harness/ChainlinkHarness.sol +++ b/test/harness/ChainlinkHarness.sol @@ -4,8 +4,7 @@ pragma solidity 0.8.16; import { Chainlink, IChainlinkFeedRegistryLike, - IChainlinkAggregatorLike, - ChainlinkDenominations + IChainlinkAggregatorLike } from "pwn/loan/lib/Chainlink.sol"; @@ -15,44 +14,45 @@ contract ChainlinkHarness { return Chainlink.checkSequencerUptime(l2SequencerUptimeFeed); } - function fetchPricesWithCommonDenominator( + function fetchCreditPriceWithCollateralDenomination( IChainlinkFeedRegistryLike feedRegistry, address creditAsset, - address collateralAsset - ) external view returns (uint256, uint256) { - return Chainlink.fetchPricesWithCommonDenominator(feedRegistry, creditAsset, collateralAsset); + address collateralAsset, + address[] memory feedIntermediaryDenominations, + bool[] memory feedInvertFlags + ) external view returns (uint256, uint8) { + return Chainlink.fetchCreditPriceWithCollateralDenomination( + feedRegistry, creditAsset, collateralAsset, feedIntermediaryDenominations, feedInvertFlags + ); } - function findPrice(IChainlinkFeedRegistryLike feedRegistry, address asset) - external - view - returns (uint256, uint8, address) - { - return Chainlink.findPrice(feedRegistry, asset); + function convertPriceDenomination( + IChainlinkFeedRegistryLike feedRegistry, + uint256 currentPrice, + uint8 currentDecimals, + address currentDenomination, + address nextDenomination, + bool nextInvert + ) external view returns (uint256, uint8) { + return Chainlink.convertPriceDenomination( + feedRegistry, currentPrice, currentDecimals, currentDenomination, nextDenomination, nextInvert + ); } - function fetchPrice(IChainlinkFeedRegistryLike feedRegistry, address asset, address denominator) + function fetchPrice(IChainlinkFeedRegistryLike feedRegistry, address asset, address denomination) external view - returns (bool, uint256, uint8) + returns (uint256, uint8) { - return Chainlink.fetchPrice(feedRegistry, asset, denominator); + return Chainlink.fetchPrice(feedRegistry, asset, denomination); } - function convertPriceDenominator( - IChainlinkFeedRegistryLike feedRegistry, - uint256 nominatorPrice, - uint8 nominatorDecimals, - address originalDenominator, - address newDenominator - ) external view returns (bool, uint256, uint8) { - return Chainlink.convertPriceDenominator( - feedRegistry, nominatorPrice, nominatorDecimals, originalDenominator, newDenominator - ); - } - - function scalePrice(uint256 price, uint8 priceDecimals, uint8 newDecimals) external pure returns (uint256) { - return Chainlink.scalePrice(price, priceDecimals, newDecimals); + function syncDecimalsUp(uint256 price1, uint8 decimals1, uint256 price2, uint8 decimals2) + external + pure + returns (uint256, uint256, uint8) + { + return Chainlink.syncDecimalsUp(price1, decimals1, price2, decimals2); } } diff --git a/test/helper/ChainlinkDenominations.sol b/test/helper/ChainlinkDenominations.sol new file mode 100644 index 0000000..bf00f0e --- /dev/null +++ b/test/helper/ChainlinkDenominations.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity 0.8.16; + +library ChainlinkDenominations { + address public constant ETH = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public constant BTC = 0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB; + address public constant USD = address(840); +} diff --git a/test/unit/Chainlink.t.sol b/test/unit/Chainlink.t.sol index 4796400..917c38a 100644 --- a/test/unit/Chainlink.t.sol +++ b/test/unit/Chainlink.t.sol @@ -7,11 +7,11 @@ import { Chainlink, IChainlinkFeedRegistryLike, IChainlinkAggregatorLike, - ChainlinkDenominations, Math } from "pwn/loan/lib/Chainlink.sol"; import { ChainlinkHarness } from "test/harness/ChainlinkHarness.sol"; +import { ChainlinkDenominations } from "test/helper/ChainlinkDenominations.sol"; abstract contract ChainlinkTest is Test { @@ -135,298 +135,152 @@ contract Chainlink_CheckSequencerUptime_Test is ChainlinkTest { /*----------------------------------------------------------*| -|* # FETCH PRICE WITH COMMON DENOMINATOR *| +|* # FETCH CREDIT PRICE WITH COLLATERAL DENOMINATION *| |*----------------------------------------------------------*/ -contract Chainlink_FetchPricesWithCommonDenominator_Test is ChainlinkTest { +contract Chainlink_FetchCreditPriceWithCollateralDenomination_Test is ChainlinkTest { address credAddr = makeAddr("credAddr"); address collAddr = makeAddr("collAddr"); - function test_shouldFetchCreditAndCollateralPrices() external { - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.USD) - ); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD) - ); + function test_shouldFail_whenInvalidInputLength() external { + address[] memory feedIntermediaryDenominations; + bool[] memory feedInvertFlags; - chainlink.fetchPricesWithCommonDenominator(feedRegistry, credAddr, collAddr); - } + feedIntermediaryDenominations = new address[](0); + feedInvertFlags = new bool[](0); + vm.expectRevert(abi.encodeWithSelector(Chainlink.ChainlinkInvalidInputLenghts.selector)); + chainlink.fetchCreditPriceWithCollateralDenomination(feedRegistry, credAddr, collAddr, feedIntermediaryDenominations, feedInvertFlags); - function test_shouldFetchETHPriceInUSD_whenCreditPriceInUSD_whenCollateralPriceNotInUSD() external { - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.ETH), - "whatnot" - ); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD), - "whatnot" - ); + feedIntermediaryDenominations = new address[](5); + feedInvertFlags = new bool[](5); + vm.expectRevert(abi.encodeWithSelector(Chainlink.ChainlinkInvalidInputLenghts.selector)); + chainlink.fetchCreditPriceWithCollateralDenomination(feedRegistry, credAddr, collAddr, feedIntermediaryDenominations, feedInvertFlags); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD) - ); + feedIntermediaryDenominations = new address[](4); + feedInvertFlags = new bool[](6); + vm.expectRevert(abi.encodeWithSelector(Chainlink.ChainlinkInvalidInputLenghts.selector)); + chainlink.fetchCreditPriceWithCollateralDenomination(feedRegistry, credAddr, collAddr, feedIntermediaryDenominations, feedInvertFlags); - chainlink.fetchPricesWithCommonDenominator(feedRegistry, credAddr, collAddr); + feedIntermediaryDenominations = new address[](10); + feedInvertFlags = new bool[](6); + vm.expectRevert(abi.encodeWithSelector(Chainlink.ChainlinkInvalidInputLenghts.selector)); + chainlink.fetchCreditPriceWithCollateralDenomination(feedRegistry, credAddr, collAddr, feedIntermediaryDenominations, feedInvertFlags); } - function test_shouldFetchETHPriceInUSD_whenCreditPriceNotInUSD_whenCollateralPriceInUSD() external { - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.ETH), - "whatnot" - ); - - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD) - ); + function test_shouldFetchIntermediaryPrices() external { + address[] memory feedIntermediaryDenominations = new address[](2); + feedIntermediaryDenominations[0] = makeAddr("denom1"); + feedIntermediaryDenominations[1] = makeAddr("denom2"); - chainlink.fetchPricesWithCommonDenominator(feedRegistry, credAddr, collAddr); - } - - function test_shouldNotFetchETHPriceInUSD_whenCreditPriceInUSD_whenCollateralPriceInUSD() external { - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.ETH), - "whatnot" - ); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.ETH), - "whatnot" - ); + bool[] memory feedInvertFlags = new bool[](3); + feedInvertFlags[0] = true; + feedInvertFlags[1] = false; + feedInvertFlags[2] = true; vm.expectCall( address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD), - 0 + abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, feedIntermediaryDenominations[0], credAddr) ); - - chainlink.fetchPricesWithCommonDenominator(feedRegistry, credAddr, collAddr); - } - - function test_shouldNotFetchETHPriceInUSD_whenCreditPriceInETH_whenCollateralPriceInETH() external { - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD), - "whatnot" - ); - vm.expectCall( address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD), - 0 + abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, feedIntermediaryDenominations[0], feedIntermediaryDenominations[1]) ); - - chainlink.fetchPricesWithCommonDenominator(feedRegistry, credAddr, collAddr); - } - - function test_shouldFail_whenNoCommonDenominator() external { - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.ETH), - "whatnot" - ); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( + vm.expectCall( address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD), - "whatnot" + abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, feedIntermediaryDenominations[1]) ); - vm.expectRevert( - abi.encodeWithSelector( - Chainlink.ChainlinkFeedCommonDenominatorNotFound.selector, credAddr, collAddr - ) - ); - chainlink.fetchPricesWithCommonDenominator(feedRegistry, credAddr, collAddr); - } - - function test_shouldScaleCreditDecimalsUp_whenCollateralHasBiggerDecimals() external { - address credAggregator = makeAddr("credAggregator"); - _mockFeed(credAggregator, credAddr, ChainlinkDenominations.USD); - _mockLastRoundData(credAggregator, 1e6, block.timestamp); - _mockFeedDecimals(credAggregator, 6); - - address collAggregator = makeAddr("collAggregator"); - _mockFeed(collAggregator, collAddr, ChainlinkDenominations.USD); - _mockLastRoundData(collAggregator, 1e18, block.timestamp); - _mockFeedDecimals(collAggregator, 18); - - (uint256 credPrice, uint256 collPrice) - = chainlink.fetchPricesWithCommonDenominator(feedRegistry, credAddr, collAddr); - - assertEq(credPrice, 1e18); - assertEq(collPrice, 1e18); - } - - function test_shouldScaleCollateralDecimalsUp_whenCreditHasBiggerDecimals() external { - address credAggregator = makeAddr("credAggregator"); - _mockFeed(credAggregator, credAddr, ChainlinkDenominations.USD); - _mockLastRoundData(credAggregator, 1e18, block.timestamp); - _mockFeedDecimals(credAggregator, 18); - - address collAggregator = makeAddr("collAggregator"); - _mockFeed(collAggregator, collAddr, ChainlinkDenominations.USD); - _mockLastRoundData(collAggregator, 1e6, block.timestamp); - _mockFeedDecimals(collAggregator, 6); - - (uint256 credPrice, uint256 collPrice) - = chainlink.fetchPricesWithCommonDenominator(feedRegistry, credAddr, collAddr); - - assertEq(credPrice, 1e18); - assertEq(collPrice, 1e18); + chainlink.fetchCreditPriceWithCollateralDenomination(feedRegistry, credAddr, collAddr, feedIntermediaryDenominations, feedInvertFlags); } } /*----------------------------------------------------------*| -|* # FIND PRICE *| +|* # CONVERT PRICE DENOMINATION *| |*----------------------------------------------------------*/ -contract Chainlink_FindPrice_Test is ChainlinkTest { +contract Chainlink_ConvertPriceDenomination_Test is ChainlinkTest { - function testFuzz_shouldFetchUSDPrice_whenAvailable(uint256 _price, uint8 _decimals) external { - _price = bound(_price, 0, uint256(type(int256).max)); + address oDenominator = makeAddr("originalDenomination"); + address nDenominator = makeAddr("newDenomination"); - _mockLastRoundData(aggregator, int256(_price), 1); - _mockFeedDecimals(aggregator, _decimals); + function test_shouldFetchIntermediaryPriceFeed_whenNotInverted() external { vm.expectCall( address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.USD), - 1 - ); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.ETH), - 0 + abi.encodeWithSelector( + IChainlinkFeedRegistryLike.getFeed.selector, oDenominator, nDenominator + ) ); + + chainlink.convertPriceDenomination(feedRegistry, 1e18, 18, oDenominator, nDenominator, false); + } + + function test_shouldFetchIntermediaryPriceFeed_whenInverted() external { vm.expectCall( address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.BTC), - 0 + abi.encodeWithSelector( + IChainlinkFeedRegistryLike.getFeed.selector, nDenominator, oDenominator + ) ); - (uint256 price, uint8 decimals, address denominator) = Chainlink.findPrice(feedRegistry, asset); - assertEq(price, _price); - assertEq(decimals, _decimals); - assertEq(denominator, ChainlinkDenominations.USD); + chainlink.convertPriceDenomination(feedRegistry, 1e18, 18, oDenominator, nDenominator, true); } - function testFuzz_shouldFetchETHPrice_whenUSDNotAvailable(uint256 _price, uint8 _decimals) external { - _price = bound(_price, 0, uint256(type(int256).max)); + function test_shouldScaleToBiggerDecimals() external { + _mockFeedDecimals(aggregator, 10); + (, uint8 decimals) + = chainlink.convertPriceDenomination(feedRegistry, 1, 6, oDenominator, nDenominator, false); + assertEq(decimals, 10); - _mockLastRoundData(aggregator, int256(_price), 1); - _mockFeedDecimals(aggregator, _decimals); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.USD), - "whatnot" - ); + _mockFeedDecimals(aggregator, 6); + (, decimals) + = chainlink.convertPriceDenomination(feedRegistry, 1, 18, oDenominator, nDenominator, false); + assertEq(decimals, 18); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.USD), - 1 - ); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.ETH), - 1 - ); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.BTC), - 0 - ); - - (uint256 price, uint8 decimals, address denominator) = chainlink.findPrice(feedRegistry, asset); - assertEq(price, _price); - assertEq(decimals, _decimals); - assertEq(denominator, ChainlinkDenominations.ETH); + _mockFeedDecimals(aggregator, 8); + (, decimals) + = chainlink.convertPriceDenomination(feedRegistry, 1, 8, oDenominator, nDenominator, false); + assertEq(decimals, 8); } - function testFuzz_shouldFetchBTCPrice_whenUSDNotAvailable_whenETHNotAvailable(uint256 _price, uint8 _decimals) external { - _price = bound(_price, 0, uint256(type(int256).max)); + function test_shouldConvertPrice_whenNotInverted() external { + _mockFeedDecimals(aggregator, 8); - _mockLastRoundData(aggregator, int256(_price), 1); - _mockFeedDecimals(aggregator, _decimals); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.ETH), - "whatnot" - ); + _mockLastRoundData(aggregator, 3000e8, 1); + (uint256 price,) = chainlink.convertPriceDenomination(feedRegistry, 6000e8, 8, oDenominator, nDenominator, false); + assertEq(price, 18000000e8); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.USD), - 1 - ); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.ETH), - 1 - ); - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.BTC), - 1 - ); + _mockLastRoundData(aggregator, 500e8, 1); + (price,) = chainlink.convertPriceDenomination(feedRegistry, 100e8, 8, oDenominator, nDenominator, false); + assertEq(price, 50000e8); - (uint256 price, uint8 decimals, address denominator) = Chainlink.findPrice(feedRegistry, asset); - assertEq(price, _price); - assertEq(decimals, _decimals); - assertEq(denominator, ChainlinkDenominations.BTC); + _mockLastRoundData(aggregator, 5000e8, 1); + (price,) = chainlink.convertPriceDenomination(feedRegistry, 1e8, 8, oDenominator, nDenominator, false); + assertEq(price, 5000e8); + + _mockLastRoundData(aggregator, 0.05e8, 1); + (price,) = chainlink.convertPriceDenomination(feedRegistry, 10e8, 8, oDenominator, nDenominator, false); + assertEq(price, 0.5e8); } - function test_shouldFail_whenUSDNotAvailable_whenETHNotAvailable_whenBTCNotAvailable() external { - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.ETH), - "whatnot" - ); - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, ChainlinkDenominations.BTC), - "whatnot" - ); + function test_shouldConvertPrice_whenInverted() external { + _mockFeedDecimals(aggregator, 8); + + _mockLastRoundData(aggregator, 3000e8, 1); + (uint256 price,) = chainlink.convertPriceDenomination(feedRegistry, 6000e8, 8, oDenominator, nDenominator, true); + assertEq(price, 2e8); + + _mockLastRoundData(aggregator, 500e8, 1); + (price,) = chainlink.convertPriceDenomination(feedRegistry, 100e8, 8, oDenominator, nDenominator, true); + assertEq(price, 0.2e8); - vm.expectRevert(abi.encodeWithSelector(Chainlink.ChainlinkFeedNotFound.selector, asset)); - chainlink.findPrice(feedRegistry, asset); + _mockLastRoundData(aggregator, 5000e8, 1); + (price,) = chainlink.convertPriceDenomination(feedRegistry, 1e8, 8, oDenominator, nDenominator, true); + assertEq(price, 0.0002e8); } } @@ -440,6 +294,7 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { address denominator = makeAddr("denominator"); + function testFuzz_shouldGetFeedFromRegistry(address _asset, address _denominator) external { vm.expectCall( address(feedRegistry), @@ -449,17 +304,15 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { chainlink.fetchPrice(feedRegistry, _asset, _denominator); } - function test_shouldReturnFalse_whenAggregatorNotRegistered() external { + function test_shouldFail_whenAggregatorNotRegistered() external { vm.mockCallRevert( address(feedRegistry), abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, asset, denominator), "whatnot" ); - (bool success, uint256 price, uint8 decimals) = chainlink.fetchPrice(feedRegistry, asset, denominator); - assertFalse(success); - assertEq(price, 0); - assertEq(decimals, 0); + vm.expectRevert("whatnot"); + chainlink.fetchPrice(feedRegistry, asset, denominator); } function test_shouldFail_whenNegativePrice() external { @@ -467,7 +320,7 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { vm.expectRevert( abi.encodeWithSelector( - Chainlink.ChainlinkFeedReturnedNegativePrice.selector, asset, denominator, -1 + Chainlink.ChainlinkFeedReturnedNegativePrice.selector, aggregator, -1, 1 ) ); chainlink.fetchPrice(feedRegistry, asset, denominator); @@ -480,7 +333,7 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { vm.expectRevert( abi.encodeWithSelector( - Chainlink.ChainlinkFeedPriceTooOld.selector, asset, 1 + Chainlink.ChainlinkFeedPriceTooOld.selector, aggregator, 1 ) ); chainlink.fetchPrice(feedRegistry, asset, denominator); @@ -492,9 +345,8 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { _mockFeedDecimals(aggregator, _decimals); _mockLastRoundData(aggregator, int256(_price), 1); - (bool success, uint256 price, uint8 decimals) = chainlink.fetchPrice(feedRegistry, asset, denominator); + (uint256 price, uint8 decimals) = chainlink.fetchPrice(feedRegistry, asset, denominator); - assertTrue(success); assertEq(price, _price); assertEq(decimals, _decimals); } @@ -503,92 +355,35 @@ contract Chainlink_FetchPrice_Test is ChainlinkTest { /*----------------------------------------------------------*| -|* # CONVERT PRICE DENOMINATOR *| +|* # SYNC DECIMALS UP *| |*----------------------------------------------------------*/ -contract Chainlink_ConvertPriceDenominator_Test is ChainlinkTest { +contract Chainlink_SyncDecimalsUp_Test is ChainlinkTest { - address oDenominator = makeAddr("originalDenominator"); - address nDenominator = makeAddr("newDenominator"); - - function test_shouldFetchConverterPriceFeed() external { - vm.expectCall( - address(feedRegistry), - abi.encodeWithSelector( - IChainlinkFeedRegistryLike.getFeed.selector, nDenominator, oDenominator - ) - ); - - chainlink.convertPriceDenominator(feedRegistry, 1e18, 18, oDenominator, nDenominator); - } - - function testFuzz_shouldReturnSameValues_whenFailedToFetchPrice(uint256 nPrice, uint8 nDecimals) external { - vm.mockCallRevert( - address(feedRegistry), - abi.encodeWithSelector( - IChainlinkFeedRegistryLike.getFeed.selector, nDenominator, oDenominator - ), - "whatnot" - ); + function test_shouldUpdateDecimals() external { + uint256 price1; + uint256 price2; + uint8 decimals; - (bool success, uint256 price, uint8 decimals) - = chainlink.convertPriceDenominator(feedRegistry, nPrice, nDecimals, oDenominator, nDenominator); + (price1, price2, decimals) = chainlink.syncDecimalsUp(1, 0, 100, 3); + assertEq(price1, 1000); + assertEq(price2, 100); + assertEq(decimals, 3); - assertFalse(success); - assertEq(price, nPrice); - assertEq(decimals, nDecimals); - } - - function testFuzz_shouldScaleToBiggerDecimals(uint8 nDecimals, uint8 feedDecimals) external { - feedDecimals = uint8(bound(feedDecimals, 0, 70)); - nDecimals = uint8(bound(nDecimals, 0, 70)); - uint8 resultDecimals = uint8(Math.max(nDecimals, feedDecimals)); - - _mockLastRoundData(aggregator, int256(10 ** feedDecimals), 1); - _mockFeedDecimals(aggregator, feedDecimals); - - (, uint256 price, uint8 decimals) - = chainlink.convertPriceDenominator(feedRegistry, 10 ** nDecimals, nDecimals, oDenominator, nDenominator); - - assertEq(price, 10 ** resultDecimals); - assertEq(decimals, resultDecimals); - } - - function test_shouldConvertPrice() external { - _mockFeedDecimals(aggregator, 8); - - _mockLastRoundData(aggregator, 3000e8, 1); - (, uint256 price, uint8 decimals) = chainlink.convertPriceDenominator(feedRegistry, 6000e8, 8, oDenominator, nDenominator); - assertEq(price, 2e8); - - _mockLastRoundData(aggregator, 500e8, 1); - (, price, decimals) = chainlink.convertPriceDenominator(feedRegistry, 100e8, 8, oDenominator, nDenominator); - assertEq(price, 0.2e8); - - _mockLastRoundData(aggregator, 5000e8, 1); - (, price, decimals) = chainlink.convertPriceDenominator(feedRegistry, 1e8, 8, oDenominator, nDenominator); - assertEq(price, 0.0002e8); - } - - function test_shouldReturnSuccess() external { - (bool success,,) = chainlink.convertPriceDenominator(feedRegistry, 1e18, 18, oDenominator, nDenominator); - assertTrue(success); - } - -} - - -/*----------------------------------------------------------*| -|* # SCALE PRICE *| -|*----------------------------------------------------------*/ + (price1, price2, decimals) = chainlink.syncDecimalsUp(5e18, 18, 0, 21); + assertEq(price1, 5e21); + assertEq(price2, 0); + assertEq(decimals, 21); -contract Chainlink_ScalePrice_Test is ChainlinkTest { + (price1, price2, decimals) = chainlink.syncDecimalsUp(3319200, 3, 3, 1); + assertEq(price1, 3319200); + assertEq(price2, 300); + assertEq(decimals, 3); - function test_shouldUpdateValueDecimals() external { - assertEq(chainlink.scalePrice(1e18, 18, 19), 1e19); - assertEq(chainlink.scalePrice(5e18, 18, 17), 5e17); - assertEq(chainlink.scalePrice(3319200, 3, 1), 33192); - assertEq(chainlink.scalePrice(0, 1, 10), 0); + (price1, price2, decimals) = chainlink.syncDecimalsUp(1e18, 18, 21e17, 18); + assertEq(price1, 1e18); + assertEq(price2, 21e17); + assertEq(decimals, 18); } } diff --git a/test/unit/PWNSimpleLoanElasticChainlinkProposal.t.sol b/test/unit/PWNSimpleLoanElasticChainlinkProposal.t.sol index 9d3e78d..01970f4 100644 --- a/test/unit/PWNSimpleLoanElasticChainlinkProposal.t.sol +++ b/test/unit/PWNSimpleLoanElasticChainlinkProposal.t.sol @@ -8,10 +8,10 @@ import { PWNSimpleLoan, IChainlinkAggregatorLike, IChainlinkFeedRegistryLike, - ChainlinkDenominations, Chainlink } from "pwn/loan/terms/simple/proposal/PWNSimpleLoanElasticChainlinkProposal.sol"; +import { ChainlinkDenominations } from "test/helper/ChainlinkDenominations.sol"; import { MultiToken, Math, @@ -27,9 +27,7 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp PWNSimpleLoanElasticChainlinkProposal.ProposalValues proposalValues; address feedRegistry = makeAddr("feedRegistry"); - address generalAggregator = makeAddr("generalAggregator"); - address credAggregator = makeAddr("credAggregator"); - address collAggregator = makeAddr("collAggregator"); + address feed = makeAddr("feed"); address weth = makeAddr("weth"); address l2SequencerUptimeFeed = makeAddr("l2SequencerUptimeFeed"); @@ -43,6 +41,9 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp proposalContract = new PWNSimpleLoanElasticChainlinkProposal(hub, revokedNonce, config, utilizedCredit, feedRegistry, address(0), weth); proposalContractAddr = PWNSimpleLoanProposal(proposalContract); + bool[] memory feedInvertFlags = new bool[](1); + feedInvertFlags[0] = false; + proposal = PWNSimpleLoanElasticChainlinkProposal.Proposal({ collateralCategory: MultiToken.Category.ERC1155, collateralAddress: token, @@ -50,6 +51,8 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp checkCollateralStateFingerprint: true, collateralStateFingerprint: keccak256("some state fingerprint"), creditAddress: token, + feedIntermediaryDenominations: new address[](0), + feedInvertFlags: feedInvertFlags, loanToValue: 10000, // 100% minCreditAmount: 1, availableCreditLimit: 0, @@ -72,9 +75,9 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp creditAmount: 1000 }); - _mockFeed(generalAggregator); - _mockLastRoundData(generalAggregator, 1e18, 1); - _mockFeedDecimals(generalAggregator, 18); + _mockFeed(feed); + _mockLastRoundData(feed, 1e18, 1); + _mockFeedDecimals(feed, 18); _mockSequencerUptimeFeed(true, block.timestamp - 1); } @@ -90,7 +93,7 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp proposalContractAddr )), keccak256(abi.encodePacked( - keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,uint256 loanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 fixedInterestAmount,uint24 accruingInterestAPR,uint32 durationOrDate,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), + keccak256("Proposal(uint8 collateralCategory,address collateralAddress,uint256 collateralId,bool checkCollateralStateFingerprint,bytes32 collateralStateFingerprint,address creditAddress,address[] feedIntermediaryDenominations,bool[] feedInvertFlags,uint256 loanToValue,uint256 minCreditAmount,uint256 availableCreditLimit,bytes32 utilizedCreditId,uint256 fixedInterestAmount,uint24 accruingInterestAPR,uint32 durationOrDate,uint40 expiration,address allowedAcceptor,address proposer,bytes32 proposerSpecHash,bool isOffer,uint256 refinancingLoanId,uint256 nonceSpace,uint256 nonce,address loanContract)"), abi.encode(_proposal) )) )); @@ -120,7 +123,7 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp function _callAcceptProposalWith(Params memory _params) internal override returns (bytes32, PWNSimpleLoan.Terms memory) { - _mockLastRoundData(generalAggregator, 1e18, block.timestamp); // To avoid "ChainlinkFeedPriceTooOld" error + _mockLastRoundData(feed, 1e18, block.timestamp); // To avoid "ChainlinkFeedPriceTooOld" error _updateProposal(_params.common); return proposalContract.acceptProposal({ @@ -137,33 +140,33 @@ abstract contract PWNSimpleLoanElasticChainlinkProposalTest is PWNSimpleLoanProp return _proposalHash(proposal); } - function _mockFeed(address aggregator) internal { + function _mockFeed(address _feed) internal { vm.mockCall( feedRegistry, abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector), - abi.encode(aggregator) + abi.encode(_feed) ); } - function _mockFeed(address aggregator, address base, address quote) internal { + function _mockFeed(address _feed, address base, address quote) internal { vm.mockCall( feedRegistry, abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, base, quote), - abi.encode(aggregator) + abi.encode(_feed) ); } - function _mockLastRoundData(address aggregator, int256 answer, uint256 updatedAt) internal { + function _mockLastRoundData(address _feed, int256 answer, uint256 updatedAt) internal { vm.mockCall( - aggregator, + _feed, abi.encodeWithSelector(IChainlinkAggregatorLike.latestRoundData.selector), abi.encode(0, answer, 0, updatedAt, 0) ); } - function _mockFeedDecimals(address aggregator, uint8 decimals) internal { + function _mockFeedDecimals(address _feed, uint8 decimals) internal { vm.mockCall( - aggregator, + _feed, abi.encodeWithSelector(IChainlinkAggregatorLike.decimals.selector), abi.encode(decimals) ); @@ -287,6 +290,14 @@ contract PWNSimpleLoanElasticChainlinkProposal_DecodeProposalData_Test is PWNSim assertEq(_proposal.checkCollateralStateFingerprint, proposal.checkCollateralStateFingerprint); assertEq(_proposal.collateralStateFingerprint, proposal.collateralStateFingerprint); assertEq(_proposal.creditAddress, proposal.creditAddress); + assertEq(_proposal.feedIntermediaryDenominations.length, proposal.feedIntermediaryDenominations.length); + for (uint256 i; i < _proposal.feedIntermediaryDenominations.length; ++i) { + assertEq(_proposal.feedIntermediaryDenominations[i], proposal.feedIntermediaryDenominations[i]); + } + assertEq(_proposal.feedInvertFlags.length, proposal.feedInvertFlags.length); + for (uint256 i; i < _proposal.feedInvertFlags.length; ++i) { + assertEq(_proposal.feedInvertFlags[i], proposal.feedInvertFlags[i]); + } assertEq(_proposal.loanToValue, proposal.loanToValue); assertEq(_proposal.minCreditAmount, proposal.minCreditAmount); assertEq(_proposal.availableCreditLimit, proposal.availableCreditLimit); @@ -317,31 +328,22 @@ contract PWNSimpleLoanElasticChainlinkProposal_GetCollateralAmount_Test is PWNSi address collAddr = makeAddr("collAddr"); address credAddr = makeAddr("credAddr"); - uint256 credAmount = 100e8; + uint256 credAmount = 100e18; uint256 loanToValue = 5000; // 50% + address[] feedIntermediaryDenominations; + bool[] feedInvertFlags; uint256 L2_GRACE_PERIOD; function setUp() virtual override public { super.setUp(); + feedIntermediaryDenominations = new address[](0); + feedInvertFlags = new bool[](1); + feedInvertFlags[0] = false; L2_GRACE_PERIOD = Chainlink.L2_GRACE_PERIOD; _mockAssetDecimals(collAddr, 18); _mockAssetDecimals(credAddr, 18); - - _mockFeed(collAggregator, collAddr, ChainlinkDenominations.USD); - _mockFeed(collAggregator, collAddr, ChainlinkDenominations.ETH); - _mockLastRoundData(collAggregator, 1e18, 1); - _mockFeedDecimals(collAggregator, 18); - - _mockFeed(credAggregator, credAddr, ChainlinkDenominations.USD); - _mockFeed(credAggregator, credAddr, ChainlinkDenominations.ETH); - _mockLastRoundData(credAggregator, 1e18, 1); - _mockFeedDecimals(credAggregator, 18); - - _mockFeed(generalAggregator, ChainlinkDenominations.ETH, ChainlinkDenominations.USD); - _mockLastRoundData(generalAggregator, 1e18, 1); - _mockFeedDecimals(generalAggregator, 18); } @@ -350,15 +352,14 @@ contract PWNSimpleLoanElasticChainlinkProposal_GetCollateralAmount_Test is PWNSi proposalContract = new PWNSimpleLoanElasticChainlinkProposal(hub, revokedNonce, config, utilizedCredit, feedRegistry, l2SequencerUptimeFeed, weth); _mockSequencerUptimeFeed(true, block.timestamp - L2_GRACE_PERIOD - 1); - _mockLastRoundData(collAggregator, 1e18, block.timestamp); - _mockLastRoundData(credAggregator, 1e18, block.timestamp); + _mockLastRoundData(feed, 1e18, block.timestamp); vm.expectCall( l2SequencerUptimeFeed, abi.encodeWithSelector(IChainlinkAggregatorLike.latestRoundData.selector) ); - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); + proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, feedIntermediaryDenominations, feedInvertFlags, loanToValue); } function test_shouldFail_whenL2SequencerDown_whenFeedSet() external { @@ -368,7 +369,7 @@ contract PWNSimpleLoanElasticChainlinkProposal_GetCollateralAmount_Test is PWNSi _mockSequencerUptimeFeed(false, block.timestamp - L2_GRACE_PERIOD - 1); vm.expectRevert(abi.encodeWithSelector(Chainlink.L2SequencerDown.selector)); - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); + proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, feedIntermediaryDenominations, feedInvertFlags, loanToValue); } function testFuzz_shouldFail_whenL2SequencerUp_whenInGracePeriod_whenFeedSet(uint256 startedAt) external { @@ -384,7 +385,7 @@ contract PWNSimpleLoanElasticChainlinkProposal_GetCollateralAmount_Test is PWNSi block.timestamp - startedAt, L2_GRACE_PERIOD ) ); - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); + proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, feedIntermediaryDenominations, feedInvertFlags, loanToValue); } function test_shouldNotFetchSequencerUptimeFeed_whenFeedNotSet() external { @@ -394,314 +395,112 @@ contract PWNSimpleLoanElasticChainlinkProposal_GetCollateralAmount_Test is PWNSi 0 ); - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); - } - - function test_shouldFetchCreditAndCollateralPrices() external { - vm.expectCall( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.USD) - ); - vm.expectCall( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD) - ); - - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); + proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, feedIntermediaryDenominations, feedInvertFlags, loanToValue); } - function test_shouldFetchETHPrice_whenWETH() external { - _mockAssetDecimals(weth, 18); + function test_shouldFail_whenIntermediaryDenominationsOutOfBounds() external { + feedIntermediaryDenominations = new address[](3); - vm.expectCall( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD) - ); - proposalContract.getCollateralAmount(weth, credAmount, collAddr, loanToValue); - - vm.expectCall( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD) + vm.expectRevert( + abi.encodeWithSelector( + PWNSimpleLoanElasticChainlinkProposal.IntermediaryDenominationsOutOfBounds.selector, + 3, proposalContract.MAX_INTERMEDIARY_DENOMINATIONS() + ) ); - proposalContract.getCollateralAmount(credAddr, credAmount, weth, loanToValue); + proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, feedIntermediaryDenominations, feedInvertFlags, loanToValue); } - function test_shouldFetchETHPriceInUSD_whenCreditPriceInUSD_whenCollateralPriceNotInUSD() external { - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.ETH), - "whatnot" - ); - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD), - "whatnot" - ); + function test_shouldFetchCreditAndCollateralPrices() external { + feedIntermediaryDenominations = new address[](2); + feedIntermediaryDenominations[0] = makeAddr("inter1"); + feedIntermediaryDenominations[1] = makeAddr("inter2"); + feedInvertFlags = new bool[](3); + feedInvertFlags[0] = false; + feedInvertFlags[1] = false; + feedInvertFlags[2] = false; vm.expectCall( feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD) + abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, feedIntermediaryDenominations[0]) ); - - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); - } - - function test_shouldFetchETHPriceInUSD_whenCreditPriceNotInUSD_whenCollateralPriceInUSD() external { - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( + vm.expectCall( feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.ETH), - "whatnot" + abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, feedIntermediaryDenominations[0], feedIntermediaryDenominations[1]) ); - vm.expectCall( feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD) + abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, feedIntermediaryDenominations[1], collAddr) ); - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); + proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, feedIntermediaryDenominations, feedInvertFlags, loanToValue); } - function test_shouldNotFetchETHPriceInUSD_whenCreditPriceInUSD_whenCollateralPriceInUSD() external { - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.ETH), - "whatnot" - ); - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.ETH), - "whatnot" - ); + function test_shouldFetchETHPrice_whenWETH() external { + _mockAssetDecimals(weth, 18); vm.expectCall( feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD), - 0 - ); - - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); - } - - function test_shouldNotFetchETHPriceInUSD_whenCreditPriceInETH_whenCollateralPriceInETH() external { - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD), - "whatnot" + abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, collAddr) ); + proposalContract.getCollateralAmount(weth, credAmount, collAddr, feedIntermediaryDenominations, feedInvertFlags, loanToValue); vm.expectCall( feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD), - 0 + abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.ETH) ); - - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); + proposalContract.getCollateralAmount(credAddr, credAmount, weth, feedIntermediaryDenominations, feedInvertFlags, loanToValue); } - function test_shouldFail_whenNoCommonDenominator() external { - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.ETH), - "whatnot" - ); - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, ChainlinkDenominations.ETH, ChainlinkDenominations.USD), - "whatnot" - ); + function test_shouldReturnCorrectDecimals() external { + // price = 1 - vm.expectRevert( - abi.encodeWithSelector( - Chainlink.ChainlinkFeedCommonDenominatorNotFound.selector, credAddr, collAddr - ) - ); - proposalContract.getCollateralAmount(credAddr, credAmount, collAddr, loanToValue); - } - - function test_shouldReturnCollateralAmount_whenBothPricesInUSD() external { - _mockLastRoundData(credAggregator, 1e8, 1); - _mockFeedDecimals(credAggregator, 8); - _mockLastRoundData(collAggregator, 1e18, 1); - _mockFeedDecimals(collAggregator, 18); - assertEq( - proposalContract.getCollateralAmount(credAddr, 9876, collAddr, 10000), - 9876 - ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 6890, collAddr, 5000), - 13780 - ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 5000, collAddr, 100), - 500000 - ); - - _mockLastRoundData(credAggregator, 1e25, 1); - _mockFeedDecimals(credAggregator, 25); - _mockLastRoundData(collAggregator, 200e18, 1); - _mockFeedDecimals(collAggregator, 18); - assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 10000), - 0.5e18 - ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 5000), - 1e18 - ); + _mockAssetDecimals(collAddr, 18); + _mockAssetDecimals(credAddr, 6); assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 100), - 50e18 - ); - } - - function test_shouldReturnCollateralAmount_whenBothPricesInETH() external { - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.USD), - "whatnot" - ); - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD), - "whatnot" + proposalContract.getCollateralAmount(credAddr, 8e6, collAddr, feedIntermediaryDenominations, feedInvertFlags, 2000), + 40e18 ); - _mockLastRoundData(credAggregator, 1e8, 1); - _mockFeedDecimals(credAggregator, 8); - _mockLastRoundData(collAggregator, 1e18, 1); - _mockFeedDecimals(collAggregator, 18); - assertEq( - proposalContract.getCollateralAmount(credAddr, 9876, collAddr, 10000), - 9876 - ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 6890, collAddr, 5000), - 13780 - ); + _mockAssetDecimals(collAddr, 6); + _mockAssetDecimals(credAddr, 18); assertEq( - proposalContract.getCollateralAmount(credAddr, 5000, collAddr, 100), - 500000 + proposalContract.getCollateralAmount(credAddr, 8e18, collAddr, feedIntermediaryDenominations, feedInvertFlags, 2000), + 40e6 ); - _mockLastRoundData(credAggregator, 1e25, 1); - _mockFeedDecimals(credAggregator, 25); - _mockLastRoundData(collAggregator, 200e18, 1); - _mockFeedDecimals(collAggregator, 18); - assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 10000), - 0.5e18 - ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 5000), - 1e18 - ); + _mockAssetDecimals(weth, 0); + _mockAssetDecimals(credAddr, 18); assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 100), - 50e18 + proposalContract.getCollateralAmount(credAddr, 8e18, weth, feedIntermediaryDenominations, feedInvertFlags, 2000), + 40 ); } - function test_shouldReturnCollateralAmount_whenCreditInUSD_whenCollateralInETH() external { - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, credAddr, ChainlinkDenominations.ETH), - "whatnot" - ); - vm.mockCallRevert( - feedRegistry, - abi.encodeWithSelector(IChainlinkFeedRegistryLike.getFeed.selector, collAddr, ChainlinkDenominations.USD), - "whatnot" - ); - - _mockLastRoundData(credAggregator, 2500e2, 1); - _mockFeedDecimals(credAggregator, 2); - _mockLastRoundData(collAggregator, 1e18, 1); - _mockFeedDecimals(collAggregator, 18); - _mockLastRoundData(generalAggregator, 2500e8, 1); - _mockFeedDecimals(generalAggregator, 8); - assertEq( - proposalContract.getCollateralAmount(credAddr, 9876, collAddr, 10000), - 9876 - ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 6890, collAddr, 5000), - 13780 - ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 5000, collAddr, 100), - 500000 - ); + function test_shouldReturnCollateralAmount() external { + _mockFeedDecimals(feed, 8); - _mockLastRoundData(credAggregator, 2500e25, 1); - _mockFeedDecimals(credAggregator, 25); - _mockLastRoundData(collAggregator, 200e18, 1); - _mockFeedDecimals(collAggregator, 18); - _mockLastRoundData(generalAggregator, 2500e8, 1); - _mockFeedDecimals(generalAggregator, 8); + _mockLastRoundData(feed, 300e8, 1); assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 10000), - 0.5e18 + proposalContract.getCollateralAmount(credAddr, 8e18, collAddr, feedIntermediaryDenominations, feedInvertFlags, 2000), + 12000e18 ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 5000), - 1e18 - ); - assertEq( - proposalContract.getCollateralAmount(credAddr, 100e18, collAddr, 100), - 50e18 - ); - } - - function test_shouldReturnCollateralAmountWithCorrectDecimals() external { - _mockLastRoundData(credAggregator, 1e8, 1); - _mockFeedDecimals(credAggregator, 8); - _mockLastRoundData(collAggregator, 2500e8, 1); - _mockFeedDecimals(collAggregator, 8); - _mockAssetDecimals(collAddr, 18); - _mockAssetDecimals(credAddr, 6); + _mockLastRoundData(feed, 1e8, 1); assertEq( - proposalContract.getCollateralAmount(credAddr, 500e6, collAddr, 8000), - 0.25e18 + proposalContract.getCollateralAmount(credAddr, 0, collAddr, feedIntermediaryDenominations, feedInvertFlags, 2000), + 0 ); - _mockAssetDecimals(collAddr, 6); - _mockAssetDecimals(credAddr, 18); + _mockLastRoundData(feed, 0.5e8, 1); assertEq( - proposalContract.getCollateralAmount(credAddr, 500e18, collAddr, 8000), - 0.25e6 + proposalContract.getCollateralAmount(credAddr, 20e18, collAddr, feedIntermediaryDenominations, feedInvertFlags, 8000), + 12.5e18 ); - } - function test_shouldUseZeroDecimals_whenDecimalsNotImplemented() external { - address credAddrWithoutDecimals = makeAddr("credAddrWithoutDecimals"); - vm.etch(credAddrWithoutDecimals, "bytes"); - - _mockFeed(credAggregator, credAddrWithoutDecimals, ChainlinkDenominations.USD); - _mockLastRoundData(credAggregator, 1e8, 1); - _mockFeedDecimals(credAggregator, 8); - _mockLastRoundData(collAggregator, 2500e8, 1); - _mockFeedDecimals(collAggregator, 8); - - _mockAssetDecimals(collAddr, 18); + _mockLastRoundData(feed, 4e8, 1); assertEq( - proposalContract.getCollateralAmount(credAddrWithoutDecimals, 500, collAddr, 8000), - 0.25e18 + proposalContract.getCollateralAmount(credAddr, 20e18, collAddr, feedIntermediaryDenominations, feedInvertFlags, 20000), + 40e18 ); }