diff --git a/.changeset/brave-lions-swim.md b/.changeset/brave-lions-swim.md new file mode 100644 index 000000000..527423f26 --- /dev/null +++ b/.changeset/brave-lions-swim.md @@ -0,0 +1,5 @@ +--- +"@exactly/protocol": patch +--- + +✨ adapter: use morpho flash loans diff --git a/.gas-snapshot b/.gas-snapshot index 7ac576218..d437d48fe 100644 --- a/.gas-snapshot +++ b/.gas-snapshot @@ -66,7 +66,7 @@ DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippage( DebtManagerTest:testFloatingToFixedRollHigherThanAvailableLiquidityWithSlippageWithThreePools() (gas: 2819298) DebtManagerTest:testFloatingToFixedRollWithAccurateSlippage() (gas: 1733293) DebtManagerTest:testFloatingToFixedRollWithAccurateSlippageWithPreviousPosition() (gas: 1955460) -DebtManagerTest:testFuzzRolls(uint8[4],uint8[4],uint256[4],uint40[4],uint8[4]) (runs: 256, μ: 8004418, ~: 8031281) +DebtManagerTest:testFuzzRolls(uint8[4],uint8[4],uint256[4],uint40[4],uint8[4]) (runs: 256, μ: 8018122, ~: 8071108) DebtManagerTest:testLateFixedRoll() (gas: 1455061) DebtManagerTest:testLateFixedRollWithThreeLoops() (gas: 2044387) DebtManagerTest:testLateFixedToFloatingRoll() (gas: 1449884) @@ -191,13 +191,17 @@ FirewallTest:test_allow_reverts_whenNotOriginalAllower() (gas: 171661) FirewallTest:test_disallow_disallows_whenAllowedAndOriginalAllower() (gas: 116906) FirewallTest:test_disallow_disallows_whenAllowedAndOriginalAllowerRoleIsRevoked() (gas: 217133) FirewallTest:test_disallow_reverts_whenAllowedAndNotOriginalAllower() (gas: 171638) -FlashLoanAdapterTest:test_consumeAdapter() (gas: 2334825) -FlashLoanAdapterTest:test_consumeAdapter_withWToken() (gas: 8860215) -FlashLoanAdapterTest:test_flashLoan_reverts_whenAmountsLengthNotOne() (gas: 32678) -FlashLoanAdapterTest:test_flashLoan_reverts_whenTokensLengthNotOne() (gas: 32591) -FlashLoanAdapterTest:test_setWToken_emitsWTokenSet() (gas: 80696) -FlashLoanAdapterTest:test_setWToken_reverts_whenNotAdmin() (gas: 66649) -FlashLoanAdapterTest:test_setWToken_sets_whenAdmin() (gas: 82048) +FlashLoanAdapterTest:test_flashLoan_forwardsArguments() (gas: 134172) +FlashLoanAdapterTest:test_flashLoan_lends_fullBalance_usdc() (gas: 227598) +FlashLoanAdapterTest:test_flashLoan_lends_fullBalance_weth() (gas: 179129) +FlashLoanAdapterTest:test_flashLoan_lends_usdc(uint256) (runs: 256, μ: 245117, ~: 245358) +FlashLoanAdapterTest:test_flashLoan_lends_whenCallerIsNotRecipient() (gas: 227866) +FlashLoanAdapterTest:test_flashLoan_reverts_whenAmountsLengthNotOne() (gas: 34362) +FlashLoanAdapterTest:test_flashLoan_reverts_whenOverAvailable() (gas: 83334) +FlashLoanAdapterTest:test_flashLoan_reverts_whenTokensLengthNotOne() (gas: 34856) +FlashLoanAdapterTest:test_flashLoan_reverts_whenZeroAmount() (gas: 43251) +FlashLoanAdapterTest:test_onMorphoFlashLoan_reverts_whenNotMorpho() (gas: 327823) +FlashLoanAdapterTest:test_rollFixed_rolls_withDebtRoller() (gas: 7425779) InstallmentsRouterTest:testAmountsLength() (gas: 36806) InstallmentsRouterTest:testBorrowETHToAnotherReceiver() (gas: 801979) InstallmentsRouterTest:testBorrowETHWithPermit() (gas: 1239367) @@ -220,19 +224,19 @@ IntegrationPreviewerTest:test_fixedRepayPosition_beforeMaturity() (gas: 580013) IntegrationPreviewerTest:test_fixedRepayPosition_maxUint() (gas: 79490) IntegrationPreviewerTest:test_fixedRepayPosition_saturatedFallback() (gas: 63069) IntegrationPreviewerTest:test_healthFactor() (gas: 464287) -IntegrationPreviewerTest:test_previewDeposit_(uint256) (runs: 256, μ: 1595663, ~: 1599768) +IntegrationPreviewerTest:test_previewDeposit_(uint256) (runs: 256, μ: 1595791, ~: 1599768) IntegrationPreviewerTest:test_previewHealthFactor() (gas: 5827588) IntegrationPreviewerTest:test_previewHealthFactor_nonCollateralMarket() (gas: 3511184) -IntegrationPreviewerTest:test_previewWithdraw_(uint256) (runs: 256, μ: 1655792, ~: 1660104) +IntegrationPreviewerTest:test_previewWithdraw_(uint256) (runs: 256, μ: 1655940, ~: 1660104) InterestRateModelTest:testFixedBorrowRate() (gas: 2088868) InterestRateModelTest:testFixedRateRevertAlreadyMatured() (gas: 2083135) InterestRateModelTest:testFixedRateRevertUtilizationExceeded() (gas: 2097203) InterestRateModelTest:testFloatingBorrowRate() (gas: 2082456) -InterestRateModelTest:testFuzzFixedRateGrowth(uint256,uint256,uint256,uint256) (runs: 256, μ: 2109336, ~: 2112192) -InterestRateModelTest:testFuzzFixedRateTimeSensitivity(uint256,uint256,uint256) (runs: 256, μ: 2117022, ~: 2117161) -InterestRateModelTest:testFuzzReferenceLegacyRateFixed(uint32,uint256,uint256[2],uint256[2],uint256,uint256,uint256) (runs: 256, μ: 12931216, ~: 13130880) -InterestRateModelTest:testFuzzReferenceRateFixed(uint256,uint256,uint256,uint256,uint256,uint256,(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,int256,uint256,uint256)) (runs: 256, μ: 2384630, ~: 2386873) -InterestRateModelTest:testFuzzReferenceRateFloating(uint256,uint256,(uint256,uint256,uint256,uint256,uint256,uint256,uint256)) (runs: 256, μ: 2324355, ~: 2324942) +InterestRateModelTest:testFuzzFixedRateGrowth(uint256,uint256,uint256,uint256) (runs: 256, μ: 2109239, ~: 2108779) +InterestRateModelTest:testFuzzFixedRateTimeSensitivity(uint256,uint256,uint256) (runs: 256, μ: 2117001, ~: 2117161) +InterestRateModelTest:testFuzzReferenceLegacyRateFixed(uint32,uint256,uint256[2],uint256[2],uint256,uint256,uint256) (runs: 256, μ: 12930349, ~: 13130838) +InterestRateModelTest:testFuzzReferenceRateFixed(uint256,uint256,uint256,uint256,uint256,uint256,(uint256,uint256,uint256,uint256,uint256,uint256,uint256,uint256,int256,uint256,uint256)) (runs: 256, μ: 2384716, ~: 2386888) +InterestRateModelTest:testFuzzReferenceRateFloating(uint256,uint256,(uint256,uint256,uint256,uint256,uint256,uint256,uint256)) (runs: 256, μ: 2324270, ~: 2324928) InterestRateModelTest:testMinTimeToMaturity() (gas: 2107232) InterestRateModelTest:testRevertMaxUtilizationLowerThanWad() (gas: 268402) MarketTest:testAccountLiquidityAdjustedDebt() (gas: 654879) @@ -396,7 +400,7 @@ PoolLibTest:testAtomicDepositBorrowRepayWithdraw() (gas: 45922) PoolLibTest:testBackupBorrow() (gas: 33624) PoolLibTest:testEarningsAccrual() (gas: 38952) PoolLibTest:testEarningsDistribution() (gas: 32679) -PoolLibTest:testFuzzAddRemoveAll(uint8[12]) (runs: 256, μ: 104690, ~: 106307) +PoolLibTest:testFuzzAddRemoveAll(uint8[12]) (runs: 256, μ: 105413, ~: 106611) PoolLibTest:testMaturityRangeLimit() (gas: 7866) PoolLibTest:testMaturityRangeTooWide() (gas: 11656) PreviewerTest:testAccountsReturningAccurateAmounts() (gas: 1687100) @@ -478,18 +482,29 @@ PriceFeedDoubleTest:testPriceFeedDoubleReturningPrice() (gas: 76193) PriceFeedDoubleTest:testPriceFeedDoubleWithActualOnChainValues() (gas: 85326) PriceFeedDoubleTest:testPriceFeedDoubleWithNegativePriceShouldRevert() (gas: 212681) PriceFeedDoubleTest:testPriceFeedDoubleWithUsdPriceFeed() (gas: 498675) -PriceFeedInverseTest:testFuzz_latestAnswer_matchesGreaterScale(int256) (runs: 256, μ: 55881, ~: 56215) -PriceFeedInverseTest:testFuzz_latestAnswer_returnsOne_whenAnswerIsAboveBaseUnitSquared(int256) (runs: 256, μ: 2531209, ~: 2531318) -PriceFeedInverseTest:testFuzz_latestAnswer_reverts_whenAnswerIsNegative(int256) (runs: 256, μ: 54861, ~: 54945) -PriceFeedInverseTest:testFuzz_latestAnswer_roundsUp(int256) (runs: 256, μ: 55095, ~: 55431) -PriceFeedInverseTest:test_assetPrice_returnsPrice_whenAnswerIsAboveBaseUnitSquared() (gas: 2509699) -PriceFeedInverseTest:test_constructor_setsPriceFeedDecimalsAndBaseUnit() (gas: 26853) -PriceFeedInverseTest:test_latestAnswer_matchesGreaterScale_withHighPrices() (gas: 229965) -PriceFeedInverseTest:test_latestAnswer_returnsExactInverse_whenDivisionIsExact() (gas: 91181) -PriceFeedInverseTest:test_latestAnswer_returnsInverse() (gas: 57926) -PriceFeedInverseTest:test_latestAnswer_returnsOne_whenAnswerIsAboveBaseUnitSquared() (gas: 91218) -PriceFeedInverseTest:test_latestAnswer_reverts_whenAnswerIsNegative() (gas: 94442) -PriceFeedInverseTest:test_latestAnswer_reverts_whenAnswerIsZero() (gas: 37201) +PriceFeedInverseTest:testFuzz_latestAnswer_matchesGreaterScale(int256) (runs: 256, μ: 123115, ~: 123460) +PriceFeedInverseTest:testFuzz_latestAnswer_returnsOne_whenAnswerIsAboveBaseUnitSquared(int256) (runs: 256, μ: 2600680, ~: 2600787) +PriceFeedInverseTest:testFuzz_latestAnswer_reverts_whenAnswerIsNegative(int256) (runs: 256, μ: 122136, ~: 122222) +PriceFeedInverseTest:testFuzz_latestAnswer_roundsUp(int256) (runs: 256, μ: 122329, ~: 122676) +PriceFeedInverseTest:testFuzz_latestRoundData_matchesLatestAnswer(int256) (runs: 256, μ: 161334, ~: 161679) +PriceFeedInverseTest:test_assetPrice_returnsPrice_whenAnswerIsAboveBaseUnitSquared() (gas: 2576966) +PriceFeedInverseTest:test_constructor_reverts_whenBaseUnitSquaredOverflows() (gas: 535510) +PriceFeedInverseTest:test_constructor_setsPriceFeedDecimalsAndBaseUnitSquared() (gas: 26921) +PriceFeedInverseTest:test_getRoundData_returnsInverse_whenRoundIsHistorical() (gas: 125280) +PriceFeedInverseTest:test_getRoundData_returnsInverse_withRoundDataUnchanged() (gas: 122279) +PriceFeedInverseTest:test_getRoundData_returnsZero_whenAnswerIsZero() (gas: 100184) +PriceFeedInverseTest:test_getRoundData_returnsZero_whenRoundHasNoData() (gas: 28035) +PriceFeedInverseTest:test_getRoundData_reverts_whenAnswerIsNegative() (gas: 120344) +PriceFeedInverseTest:test_latestAnswer_matchesGreaterScale_withHighPrices() (gas: 569057) +PriceFeedInverseTest:test_latestAnswer_returnsExactInverse_whenDivisionIsExact() (gas: 225671) +PriceFeedInverseTest:test_latestAnswer_returnsInverse() (gas: 127418) +PriceFeedInverseTest:test_latestAnswer_returnsOne_whenAnswerIsAboveBaseUnitSquared() (gas: 225730) +PriceFeedInverseTest:test_latestAnswer_reverts_whenAnswerIsNegative() (gas: 228996) +PriceFeedInverseTest:test_latestAnswer_reverts_whenAnswerIsZero() (gas: 94141) +PriceFeedInverseTest:test_latestRoundData_returnsInverse_withRoundDataUnchanged() (gas: 136077) +PriceFeedInverseTest:test_latestRoundData_reverts_whenAnswerIsNegative() (gas: 119426) +PriceFeedInverseTest:test_latestRoundData_reverts_whenAnswerIsZero() (gas: 99097) +PriceFeedInverseTest:test_phaseId_returnsSourcePhaseId() (gas: 845935) PriceFeedPoolTest:testPriceFeedPoolReturningPrice() (gas: 40917) PriceFeedPoolTest:testPriceFeedPoolReturningPriceWithToken0False() (gas: 2181230) PriceFeedPoolTest:testPriceFeedPoolWithAllDifferentDecimals() (gas: 2159319) @@ -589,8 +604,8 @@ StakedEXATest:invariantNoDuplicatedReward() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:invariantRewardsUpOnly() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:invariantShareValueIsOne() (runs: 10, calls: 5000, reverts: 0) StakedEXATest:testAlreadyListedError() (gas: 43677) -StakedEXATest:testAvgIndex(uint256[3],uint256[2]) (runs: 256, μ: 1319458, ~: 1370678) -StakedEXATest:testAvgStartTime(uint256[3],uint256[2]) (runs: 256, μ: 1247524, ~: 1298744) +StakedEXATest:testAvgIndex(uint256[3],uint256[2]) (runs: 256, μ: 1322129, ~: 1370819) +StakedEXATest:testAvgStartTime(uint256[3],uint256[2]) (runs: 256, μ: 1250195, ~: 1298885) StakedEXATest:testBalanceOfDeposit(uint80) (runs: 256, μ: 331003, ~: 339807) StakedEXATest:testBalanceOfWithdraw(uint256) (runs: 256, μ: 82393, ~: 82407) StakedEXATest:testCanChangeRewardsDurationWhenDisabled() (gas: 176331) @@ -599,12 +614,12 @@ StakedEXATest:testClaimAndUnstake() (gas: 1622618) StakedEXATest:testClaimAndWithdrawAfterRefTime() (gas: 1029325) StakedEXATest:testClaimBeforeFirstHarvest() (gas: 527334) StakedEXATest:testDepositClaimsRewardsToReceiver() (gas: 1121108) -StakedEXATest:testDepositEvent(uint256) (runs: 256, μ: 335228, ~: 334966) -StakedEXATest:testDepositShouldClaim(uint256[2],uint32) (runs: 256, μ: 826654, ~: 754952) +StakedEXATest:testDepositEvent(uint256) (runs: 256, μ: 335232, ~: 334990) +StakedEXATest:testDepositShouldClaim(uint256[2],uint32) (runs: 256, μ: 829682, ~: 754964) StakedEXATest:testDepositToAnotherWithAllowance() (gas: 375284) StakedEXATest:testDepositToAnotherWithoutAllowanceShouldFail() (gas: 121959) -StakedEXATest:testDepositWithdrawAvgStartTimeAndIndex(uint256[3],uint256,uint256[5]) (runs: 256, μ: 1875287, ~: 1904448) -StakedEXATest:testEarnedWithTime(uint256) (runs: 256, μ: 86108, ~: 86326) +StakedEXATest:testDepositWithdrawAvgStartTimeAndIndex(uint256[3],uint256,uint256[5]) (runs: 256, μ: 1885504, ~: 1906202) +StakedEXATest:testEarnedWithTime(uint256) (runs: 256, μ: 86121, ~: 86326) StakedEXATest:testEmergencyAdminCanPauseNotUnpause() (gas: 175029) StakedEXATest:testFinishDistributionEmitEvent() (gas: 240752) StakedEXATest:testFinishDistributionLetsClaimUnclaimed() (gas: 1784877) @@ -615,9 +630,9 @@ StakedEXATest:testGrantRevokeEmergencyAdmin() (gas: 117824) StakedEXATest:testGrantRevokePauser() (gas: 117836) StakedEXATest:testHandlerClaim(uint8) (runs: 256, μ: 485340, ~: 485340) StakedEXATest:testHandlerDeposit(uint80) (runs: 256, μ: 1240331, ~: 1276242) -StakedEXATest:testHandlerHarvest(uint64) (runs: 256, μ: 402351, ~: 411143) -StakedEXATest:testHandlerNotifyRewardAmount(uint64) (runs: 256, μ: 165113, ~: 156797) -StakedEXATest:testHandlerSetDuration(uint32) (runs: 256, μ: 198473, ~: 218711) +StakedEXATest:testHandlerHarvest(uint64) (runs: 256, μ: 402353, ~: 411143) +StakedEXATest:testHandlerNotifyRewardAmount(uint64) (runs: 256, μ: 165442, ~: 156797) +StakedEXATest:testHandlerSetDuration(uint32) (runs: 256, μ: 198922, ~: 218711) StakedEXATest:testHandlerSetMarket() (gas: 144553) StakedEXATest:testHandlerWithdraw(uint256) (runs: 256, μ: 91497, ~: 91511) StakedEXATest:testHarvestAmountWithReducedAllowance() (gas: 184605) @@ -628,13 +643,13 @@ StakedEXATest:testHarvestWhenFinished() (gas: 326352) StakedEXATest:testHarvestX() (gas: 180952) StakedEXATest:testHarvestZero() (gas: 255436) StakedEXATest:testInitialValues() (gas: 196484) -StakedEXATest:testInsufficientBalanceError(uint256) (runs: 256, μ: 63958, ~: 64151) +StakedEXATest:testInsufficientBalanceError(uint256) (runs: 256, μ: 63941, ~: 64151) StakedEXATest:testMaxRewardsGasConsumption() (gas: 144674963) StakedEXATest:testMultipleClaimsVsOne() (gas: 21936440) StakedEXATest:testMultipleHarvests() (gas: 284083) -StakedEXATest:testNoRewardsAfterPeriod(uint256) (runs: 256, μ: 1809893, ~: 1804427) +StakedEXATest:testNoRewardsAfterPeriod(uint256) (runs: 256, μ: 1809452, ~: 1804427) StakedEXATest:testNotPausingRoleError() (gas: 39124) -StakedEXATest:testNotifyRewardAmount(uint256,uint256) (runs: 256, μ: 155601, ~: 155564) +StakedEXATest:testNotifyRewardAmount(uint256,uint256) (runs: 256, μ: 155622, ~: 155564) StakedEXATest:testNotifyRewardWithUnderlyingAsset() (gas: 460608) StakedEXATest:testOnlyAdminEnableReward() (gas: 1297302) StakedEXATest:testOnlyAdminFinishDistribution() (gas: 213640) @@ -652,13 +667,13 @@ StakedEXATest:testPenaltyThresholdRange() (gas: 36989) StakedEXATest:testPermitAndDeposit() (gas: 452146) StakedEXATest:testPermitFailKeepsFlow() (gas: 484480) StakedEXATest:testRemoveDepositAllowance() (gas: 466913) -StakedEXATest:testResetDepositAfterRefTime(uint256) (runs: 256, μ: 1033239, ~: 1032994) -StakedEXATest:testRewardAmountNotifiedEvent(uint256) (runs: 256, μ: 105547, ~: 105293) +StakedEXATest:testResetDepositAfterRefTime(uint256) (runs: 256, μ: 1033244, ~: 1032994) +StakedEXATest:testRewardAmountNotifiedEvent(uint256) (runs: 256, μ: 105545, ~: 105293) StakedEXATest:testRewardNotListedError() (gas: 1177184) -StakedEXATest:testRewardPaidEvent(uint256,uint256) (runs: 256, μ: 809092, ~: 854321) -StakedEXATest:testRewardsAmounts(uint256) (runs: 256, μ: 1707506, ~: 1707287) +StakedEXATest:testRewardPaidEvent(uint256,uint256) (runs: 256, μ: 809633, ~: 854321) +StakedEXATest:testRewardsAmounts(uint256) (runs: 256, μ: 1707514, ~: 1707287) StakedEXATest:testRewardsDurationSetEvent(uint40) (runs: 256, μ: 52173, ~: 52149) -StakedEXATest:testSetDuration(uint256,uint40) (runs: 256, μ: 65997, ~: 64453) +StakedEXATest:testSetDuration(uint256,uint40) (runs: 256, μ: 66062, ~: 64453) StakedEXATest:testSetMarketAddressZero() (gas: 36979) StakedEXATest:testSetMarketOnlyAdmin() (gas: 1345554) StakedEXATest:testSetMaxRewardsTokensExceeded() (gas: 111958823) @@ -670,12 +685,12 @@ StakedEXATest:testSetSavingsZeroAddressError() (gas: 37006) StakedEXATest:testTotalSupplyDeposit(uint80) (runs: 256, μ: 330389, ~: 339193) StakedEXATest:testTotalSupplyWithdraw(uint256) (runs: 256, μ: 81753, ~: 81767) StakedEXATest:testUntransferable(uint80) (runs: 256, μ: 338580, ~: 348672) -StakedEXATest:testWithdrawEvent(uint256) (runs: 256, μ: 483393, ~: 483140) +StakedEXATest:testWithdrawEvent(uint256) (runs: 256, μ: 483398, ~: 483176) StakedEXATest:testWithdrawRewardUnderlyingAsset() (gas: 456617) StakedEXATest:testWithdrawRewardsOnlyAdmin() (gas: 242200) StakedEXATest:testWithdrawRewardsOnlyReward() (gas: 1177174) -StakedEXATest:testWithdrawSameAmountRewardsShouldEqual(uint256,uint256) (runs: 256, μ: 1063385, ~: 1107803) -StakedEXATest:testWithdrawWithRewards(uint256) (runs: 256, μ: 895617, ~: 895364) +StakedEXATest:testWithdrawSameAmountRewardsShouldEqual(uint256,uint256) (runs: 256, μ: 1065152, ~: 1107803) +StakedEXATest:testWithdrawWithRewards(uint256) (runs: 256, μ: 895622, ~: 895400) StakedEXATest:testZeroRateError() (gas: 58274) StakingPreviewerTest:testAllClaimable() (gas: 769205) StakingPreviewerTest:testAllClaimed() (gas: 818952) diff --git a/contracts/PriceFeedInverse.sol b/contracts/PriceFeedInverse.sol index b51e992e8..3c8c5dc0c 100644 --- a/contracts/PriceFeedInverse.sol +++ b/contracts/PriceFeedInverse.sol @@ -3,29 +3,70 @@ pragma solidity ^0.8.17; import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { SafeCastLib } from "solady/src/utils/SafeCastLib.sol"; + import { IPriceFeed } from "./utils/IPriceFeed.sol"; contract PriceFeedInverse is IPriceFeed { using FixedPointMathLib for uint256; using SafeCastLib for int256; - /// @notice Price feed whose answer is inverted. - IPriceFeed public immutable priceFeed; - /// @notice Number of decimals that the answer of this price feed has, same as the inverted feed's. + /// @notice Source price feed whose answer is inverted. + IAggregatorProxy public immutable priceFeed; + /// @notice Number of decimals that the answer of this price feed has, same as the source feed's. uint8 public immutable decimals; - /// @notice Base unit of the inverted feed's answer. - uint256 public immutable baseUnit; + /// @notice Square of the source feed's base unit, used as the numerator to invert its answer. + uint256 public immutable baseUnitSquared; - constructor(IPriceFeed priceFeed_) { + constructor(IAggregatorProxy priceFeed_) { priceFeed = priceFeed_; decimals = priceFeed_.decimals(); - baseUnit = 10 ** decimals; + baseUnitSquared = 10 ** (2 * uint256(decimals)); } - /// @notice Returns the inverse of the price feed's latest answer. + /// @notice Returns the inverse of the source feed's latest answer. /// @dev Rounds up, the conservative direction for debt-only markets, so the inverse can't truncate to zero. /// Reverts on a zero or negative answer. function latestAnswer() external view returns (int256) { - return int256(baseUnit.mulDivUp(baseUnit, priceFeed.latestAnswer().toUint256())); + return inverse(priceFeed.latestAnswer()); + } + + /// @notice Returns the source feed's data for `roundId_`, with the answer inverted like `latestAnswer`. + /// @dev A zero answer, which the source returns for rounds without data, is kept as zero. + function getRoundData( + uint80 roundId_ + ) + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + (roundId, answer, startedAt, updatedAt, answeredInRound) = priceFeed.getRoundData(roundId_); + if (answer != 0) answer = inverse(answer); + } + + /// @notice Returns the source feed's latest round data, with the answer inverted like `latestAnswer`. + function latestRoundData() + external + view + returns (uint80 roundId, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) + { + (roundId, answer, startedAt, updatedAt, answeredInRound) = priceFeed.latestRoundData(); + answer = inverse(answer); + } + + /// @notice Returns the source feed's current phase id. + function phaseId() external view returns (uint16) { + return priceFeed.phaseId(); } + + function inverse(int256 answer) internal view returns (int256) { + return int256(baseUnitSquared.divUp(answer.toUint256())); + } +} + +interface IAggregatorProxy is IPriceFeed { + function getRoundData(uint80 roundId) external view returns (uint80, int256, uint256, uint256, uint80); + + function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80); + + function phaseId() external view returns (uint16); } diff --git a/contracts/periphery/FlashLoanAdapter.sol b/contracts/periphery/FlashLoanAdapter.sol index 1aff7d959..491bf4c37 100644 --- a/contracts/periphery/FlashLoanAdapter.sol +++ b/contracts/periphery/FlashLoanAdapter.sol @@ -1,108 +1,66 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.20; -import { AccessControl } from "@openzeppelin/contracts-v4/access/AccessControl.sol"; import { IERC20 } from "@openzeppelin/contracts-v4/interfaces/IERC20.sol"; -import { IERC4626 } from "@openzeppelin/contracts-v4/interfaces/IERC4626.sol"; +import { SafeTransferLib } from "solady/src/utils/SafeTransferLib.sol"; + +import { IFlashLoaner, IFlashLoanRecipient } from "./DebtRoller.sol"; /// @title FlashLoanAdapter -/// @notice Adapter to use Balancer V3 vault with Balancer V2 interface. -/// Uses wrapped tokens when there's not enough liquidity in the vault. -contract FlashLoanAdapter is AccessControl { - IBalancerVaultV3 public immutable vault; +/// @notice Adapter to use Morpho flash loans with the `IFlashLoaner` interface. +/// Morpho flash loans are free, so reported fees are always zero. +contract FlashLoanAdapter is IFlashLoaner { + using SafeTransferLib for address; - /// @notice Mapping to check if a token is a wrapped token. - mapping(IERC20 wToken => bool isWToken) public isWToken; - /// @notice Mapping to get the wrapped token for an asset. - mapping(IERC20 asset => IERC4626 wToken) public wTokens; + /// @notice Morpho contract that provides the flash loans. + IMorpho public immutable morpho; - constructor(IBalancerVaultV3 vault_, address owner) { - _grantRole(DEFAULT_ADMIN_ROLE, owner); - vault = vault_; + constructor(IMorpho morpho_) { + morpho = morpho_; } - /// @notice Performs a flash loan using the Balancer V3 vault. + /// @notice Performs a flash loan using Morpho. /// @param recipient The address to receive the flash loan. /// @param tokens The tokens to borrow. /// @param amounts The amounts to borrow. /// @param data Additional data to pass to the recipient. - // solhint-disable-next-line gas-calldata-parameters - function flashLoan(address recipient, IERC20[] memory tokens, uint256[] memory amounts, bytes memory data) external { + function flashLoan( + address recipient, + IERC20[] calldata tokens, + uint256[] calldata amounts, + bytes calldata data + ) external { if (tokens.length != 1 || amounts.length != 1) revert InvalidLength(); - uint256[] memory fees = new uint256[](1); - if (tokens[0].balanceOf(address(vault)) < amounts[0]) { - IERC4626 wToken = wTokens[tokens[0]]; - if (wToken == IERC4626(address(0)) || wToken.convertToAssets(wToken.balanceOf(address(vault))) < amounts[0]) { - revert InsufficientLiquidity(); - } - - tokens[0] = IERC20(address(wToken)); - uint256 shares = wToken.previewWithdraw(amounts[0]); - fees[0] = wToken.previewMint(shares) - amounts[0]; - amounts[0] = shares; - } - vault.unlock( - abi.encodeWithSelector(this.receiveFlashLoan.selector, abi.encode(recipient, tokens, amounts, fees, data)) - ); + morpho.flashLoan(address(tokens[0]), amounts[0], abi.encode(recipient, tokens[0], data)); } - /// @notice Receives a flash loan from the Balancer V3 vault. - /// @param payload The payload containing the recipient, tokens, amounts, fees, and data. - function receiveFlashLoan(bytes calldata payload) external { - if (msg.sender != address(vault)) revert UnauthorizedVault(); - ( - address recipient, - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory fees, - bytes memory data - ) = abi.decode(payload, (address, IERC20[], uint256[], uint256[], bytes)); + /// @notice Receives a flash loan from Morpho, forwards it to the recipient and approves the repayment. + /// @param assets The amount of assets that was flash loaned. + /// @param payload The payload containing the recipient, token, and data. + function onMorphoFlashLoan(uint256 assets, bytes calldata payload) external { + if (msg.sender != address(morpho)) revert UnauthorizedMorpho(); + (address recipient, IERC20 token, bytes memory data) = abi.decode(payload, (address, IERC20, bytes)); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = token; + uint256[] memory amounts = new uint256[](1); + amounts[0] = assets; - if (isWToken[tokens[0]]) { - IERC4626 wToken = IERC4626(address(tokens[0])); - vault.sendTo(tokens[0], address(this), amounts[0]); - tokens[0] = IERC20(wToken.asset()); - amounts[0] = wToken.redeem(amounts[0], recipient, address(this)); - IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, fees, data); + address(token).safeTransfer(recipient, assets); + IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, new uint256[](1), data); - tokens[0].approve(address(wToken), amounts[0] + fees[0]); - wToken.deposit(amounts[0] + fees[0], address(vault)); - vault.settle(wToken, amounts[0]); - } else { - vault.sendTo(tokens[0], recipient, amounts[0]); - IFlashLoanRecipient(recipient).receiveFlashLoan(tokens, amounts, fees, data); - tokens[0].transfer(address(vault), amounts[0]); - vault.settle(tokens[0], amounts[0]); - } + address(token).safeApprove(address(morpho), assets); } - /// @notice Sets a wrapped token for an asset. - /// @param asset The asset to set the wrapped token for. - /// @param token The wrapped token to set. - function setWToken(IERC20 asset, IERC4626 token) external onlyRole(DEFAULT_ADMIN_ROLE) { - wTokens[asset] = token; - isWToken[token] = true; - emit WTokenSet(asset, token, msg.sender); + /// @notice Returns the amount of `token` available to be flash loaned. + /// @param token The token to check. + function available(IERC20 token) external view returns (uint256) { + return token.balanceOf(address(morpho)); } } -interface IBalancerVaultV3 { - function sendTo(IERC20 token, address to, uint256 amount) external; - function settle(IERC20 token, uint256 amountHint) external returns (uint256 credit); - function unlock(bytes calldata data) external returns (bytes memory); +interface IMorpho { + function flashLoan(address token, uint256 assets, bytes calldata data) external; } -interface IFlashLoanRecipient { - function receiveFlashLoan( - IERC20[] memory tokens, - uint256[] memory amounts, - uint256[] memory fees, - bytes calldata data - ) external; -} - -error InsufficientLiquidity(); error InvalidLength(); -error UnauthorizedVault(); - -event WTokenSet(IERC20 indexed asset, IERC4626 indexed wToken, address indexed account); +error UnauthorizedMorpho(); diff --git a/deploy/FlashLoan.ts b/deploy/FlashLoan.ts index 3310505b0..190cd062e 100644 --- a/deploy/FlashLoan.ts +++ b/deploy/FlashLoan.ts @@ -3,16 +3,12 @@ import type { DeployFunction } from "hardhat-deploy/types"; import tenderlify from "./.utils/tenderlify"; const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAccounts }) => { - const [{ address: flashLoaner }, { address: timelock }, { deployer }] = await Promise.all([ - get("Balancer3Vault"), - get("TimelockController"), - getNamedAccounts(), - ]); + const [{ address: morpho }, { deployer }] = await Promise.all([get("Morpho"), getNamedAccounts()]); await tenderlify( "FlashLoanAdapter", await deploy("FlashLoanAdapter", { - args: [flashLoaner, timelock], + args: [morpho], skipIfAlreadyDeployed: !JSON.parse(env[`DEPLOY_FLASH_LOAN`] ?? "false"), from: deployer, log: true, @@ -21,8 +17,6 @@ const func: DeployFunction = async ({ deployments: { deploy, get }, getNamedAcco }; func.tags = ["FlashLoan"]; -func.dependencies = ["Governance"]; -func.skip = async ({ network, deployments }) => - !!network.config.sunset || !(await deployments.getOrNull("Balancer3Vault")); +func.skip = async ({ network, deployments }) => !!network.config.sunset || !(await deployments.getOrNull("Morpho")); export default func; diff --git a/deployments/base/Balancer3Vault.json b/deployments/base/Balancer3Vault.json deleted file mode 100644 index 561eeed0b..000000000 --- a/deployments/base/Balancer3Vault.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "address": "0xbA1333333333a1BA1108E8412f11850A5C319bA9" -} diff --git a/deployments/base/Morpho.json b/deployments/base/Morpho.json new file mode 100644 index 000000000..457f9ec18 --- /dev/null +++ b/deployments/base/Morpho.json @@ -0,0 +1,3 @@ +{ + "address": "0xBBBBBbbBBb9cC5e90e3b3Af64bdAF62C37EEFFCb" +} diff --git a/deployments/optimism/Morpho.json b/deployments/optimism/Morpho.json new file mode 100644 index 000000000..f754ebe23 --- /dev/null +++ b/deployments/optimism/Morpho.json @@ -0,0 +1,3 @@ +{ + "address": "0xce95AfbB8EA029495c66020883F87aaE8864AF92" +} diff --git a/test/FlashLoanAdapter.t.sol b/test/FlashLoanAdapter.t.sol index 06039d68c..1d224f16c 100644 --- a/test/FlashLoanAdapter.t.sol +++ b/test/FlashLoanAdapter.t.sol @@ -1,117 +1,202 @@ // SPDX-License-Identifier: GPL-3.0 pragma solidity ^0.8.0; // solhint-disable-line one-contract-per-file -import { IERC20 } from "@openzeppelin/contracts-v4/interfaces/IERC20.sol"; +import { ERC1967Proxy } from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol"; import { ForkTest } from "./Fork.t.sol"; +import { FixedLib } from "../contracts/utils/FixedLib.sol"; +import { Auditor, DebtRoller, IFlashLoanRecipient, Market } from "../contracts/periphery/DebtRoller.sol"; import { - FlashLoanAdapter, - IBalancerVaultV3, - IERC4626, - IFlashLoanRecipient, + IERC20, + IMorpho, InvalidLength, - WTokenSet + FlashLoanAdapter, + UnauthorizedMorpho } from "../contracts/periphery/FlashLoanAdapter.sol"; contract FlashLoanAdapterTest is ForkTest { FlashLoanAdapter internal adapter; - IBalancerVaultV3 internal vaultV3; + FlashLoanConsumer internal consumer; + IMorpho internal morpho; + IERC20 internal usdc; + IERC20 internal weth; function setUp() external { - adapter = new FlashLoanAdapter(IBalancerVaultV3(address(0)), address(this)); + vm.createSelectFork(vm.envString("OPTIMISM_NODE"), 157_331_000); + + morpho = IMorpho(deployment("Morpho")); + usdc = IERC20(deployment("USDC")); + weth = IERC20(deployment("WETH")); + adapter = new FlashLoanAdapter(morpho); + consumer = new FlashLoanConsumer(adapter); } - function test_setWToken_sets_whenAdmin() external { - IERC20 asset = IERC20(address(0x1)); - IERC4626 wToken = IERC4626(address(0x2)); + function test_flashLoan_lends_usdc(uint256 amount) external { + amount = bound(amount, 1, adapter.available(usdc)); + uint256 morphoBalance = usdc.balanceOf(address(morpho)); - adapter.setWToken(asset, wToken); - assertEq(address(adapter.wTokens(asset)), address(wToken), "wToken not set"); + consumer.callFlashLoan(usdc, amount, ""); + + assertLent(usdc, amount, morphoBalance); } - function test_setWToken_emitsWTokenSet() external { - IERC20 asset = IERC20(address(0x1)); - IERC4626 wToken = IERC4626(address(0x2)); - vm.expectEmit(true, true, true, true, address(adapter)); - emit WTokenSet(asset, wToken, address(this)); - adapter.setWToken(asset, wToken); + function test_flashLoan_lends_fullBalance_usdc() external { + uint256 amount = adapter.available(usdc); + assertGt(amount, 0, "no usdc available"); + + consumer.callFlashLoan(usdc, amount, ""); + + assertLent(usdc, amount, amount); } - function test_setWToken_reverts_whenNotAdmin() external { - address nonAdmin = address(0x1); - vm.startPrank(nonAdmin); - vm.expectRevert(bytes("")); - adapter.setWToken(IERC20(address(0x1)), IERC4626(address(0x2))); - assertEq(address(adapter.wTokens(IERC20(address(0x1)))), address(0), "wToken set"); + function test_flashLoan_lends_fullBalance_weth() external { + uint256 amount = adapter.available(weth); + assertGt(amount, 0, "no weth available"); + + consumer.callFlashLoan(weth, amount, ""); + + assertLent(weth, amount, amount); } - function test_consumeAdapter() external { - _setUpFork(); - IERC20 rETH = IERC20(0x9Bcef72be871e61ED4fBbc7630889beE758eb81D); - FlashLoanConsumer consumer = new FlashLoanConsumer(adapter, rETH); - consumer.callFlashLoan(); + function test_flashLoan_forwardsArguments() external { + uint256 amount = 1_000e6; + bytes memory data = abi.encode("data"); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = usdc; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + vm.expectCall( + address(morpho), + abi.encodeCall(IMorpho.flashLoan, (address(usdc), amount, abi.encode(address(consumer), usdc, data))) + ); + vm.expectCall( + address(consumer), + abi.encodeCall(IFlashLoanRecipient.receiveFlashLoan, (tokens, amounts, new uint256[](1), data)) + ); + consumer.callFlashLoan(usdc, amount, data); } - function test_consumeAdapter_withWToken() external { - _setUpFork(); - IERC20 usdc = IERC20(deployment("USDC")); - IERC4626 waOptUSDCn = IERC4626(address(0x41B334E9F2C0ED1f30fD7c351874a6071C53a78E)); - adapter.setWToken(usdc, waOptUSDCn); - FlashLoanConsumer consumer = new FlashLoanConsumer(adapter, usdc); + function test_flashLoan_lends_whenCallerIsNotRecipient() external { + uint256 amount = 1_000e6; + uint256 morphoBalance = usdc.balanceOf(address(morpho)); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = usdc; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + vm.prank(makeAddr("alice")); + adapter.flashLoan(address(consumer), tokens, amounts, ""); + + assertLent(usdc, amount, morphoBalance); + } - deal(address(usdc), address(consumer), 1); - consumer.callFlashLoan(); + function test_flashLoan_reverts_whenOverAvailable() external { + uint256 amount = adapter.available(usdc) + 1; + vm.expectRevert(bytes("transfer reverted")); + consumer.callFlashLoan(usdc, amount, ""); + } - assertEq(usdc.balanceOf(address(consumer)), 0); - assertEq(usdc.balanceOf(address(adapter)), 0); + function test_flashLoan_reverts_whenZeroAmount() external { + vm.expectRevert(bytes("zero assets")); + consumer.callFlashLoan(usdc, 0, ""); } function test_flashLoan_reverts_whenTokensLengthNotOne() external { vm.expectRevert(InvalidLength.selector); - adapter.flashLoan(address(0), new IERC20[](0), new uint256[](1), ""); + adapter.flashLoan(address(consumer), new IERC20[](2), new uint256[](1), ""); } function test_flashLoan_reverts_whenAmountsLengthNotOne() external { vm.expectRevert(InvalidLength.selector); - adapter.flashLoan(address(0), new IERC20[](1), new uint256[](0), ""); + adapter.flashLoan(address(consumer), new IERC20[](1), new uint256[](0), ""); + } + + function test_onMorphoFlashLoan_reverts_whenNotMorpho() external { + uint256 amount = 1_000e6; + address attacker = makeAddr("attacker"); + deal(address(usdc), address(adapter), amount); + IERC20[] memory tokens = new IERC20[](1); + tokens[0] = usdc; + uint256[] memory amounts = new uint256[](1); + amounts[0] = amount; + + vm.prank(attacker); + vm.expectRevert(UnauthorizedMorpho.selector); + adapter.onMorphoFlashLoan(amount, abi.encode(attacker, tokens, amounts, "")); + + assertEq(usdc.balanceOf(address(adapter)), amount, "adapter lost tokens"); + assertEq(usdc.allowance(address(adapter), address(morpho)), 0, "allowance set"); } - function _setUpFork() internal { - vm.createSelectFork(vm.envString("OPTIMISM_NODE"), 141_227_400); + function test_rollFixed_rolls_withDebtRoller() external { + Auditor auditor = Auditor(deployment("Auditor")); + upgrade(address(auditor), address(new Auditor(auditor.priceDecimals()))); + Market exaUSDC = Market(deployment("MarketUSDC")); + DebtRoller debtRoller = DebtRoller(address(new ERC1967Proxy(address(new DebtRoller(auditor, adapter)), ""))); + debtRoller.initialize(); + + address bob = makeAddr("bob"); + uint256 maturity = block.timestamp - (block.timestamp % FixedLib.INTERVAL) + FixedLib.INTERVAL; + uint256 borrowAmount = 1_000e6; + deal(address(usdc), bob, 10_000e6); + + vm.startPrank(bob); + usdc.approve(address(exaUSDC), type(uint256).max); + exaUSDC.deposit(10_000e6, bob); + auditor.enterMarket(exaUSDC); + exaUSDC.borrowAtMaturity(maturity, borrowAmount, type(uint256).max, bob, bob); + + uint256 marketBalance = usdc.balanceOf(address(exaUSDC)); + uint256 morphoBalance = usdc.balanceOf(address(morpho)); + exaUSDC.approve(address(debtRoller), type(uint256).max); + debtRoller.rollFixed(exaUSDC, maturity, maturity + FixedLib.INTERVAL, borrowAmount * 2, type(uint256).max, 1e18); + vm.stopPrank(); + + (uint256 principal, uint256 fee) = exaUSDC.fixedBorrowPositions(maturity, bob); + assertEq(principal + fee, 0, "repay maturity not rolled"); + (principal, ) = exaUSDC.fixedBorrowPositions(maturity + FixedLib.INTERVAL, bob); + assertGe(principal, borrowAmount, "borrow maturity not rolled"); + assertEq(usdc.balanceOf(address(exaUSDC)), marketBalance, "flash loan fee charged"); + assertEq(usdc.balanceOf(address(morpho)), morphoBalance, "morpho not repaid"); + assertEq(usdc.balanceOf(address(debtRoller)), 0, "debt roller kept tokens"); + assertEq(usdc.balanceOf(address(adapter)), 0, "adapter kept tokens"); + } - vaultV3 = IBalancerVaultV3(0xbA1333333333a1BA1108E8412f11850A5C319bA9); - adapter = new FlashLoanAdapter(vaultV3, address(this)); + function assertLent(IERC20 token, uint256 amount, uint256 morphoBalance) internal view { + assertEq(consumer.received(), amount, "recipient not funded"); + assertEq(token.balanceOf(address(consumer)), 0, "recipient kept tokens"); + assertEq(token.balanceOf(address(morpho)), morphoBalance, "morpho not repaid"); + assertEq(token.balanceOf(address(adapter)), 0, "adapter kept tokens"); + assertEq(token.allowance(address(adapter), address(morpho)), 0, "allowance not consumed"); } } contract FlashLoanConsumer is IFlashLoanRecipient { - FlashLoanAdapter internal adapter; - IERC20 internal token; - uint256 internal prevAmount; + FlashLoanAdapter internal immutable adapter; + uint256 public received; - constructor(FlashLoanAdapter adapter_, IERC20 token_) { + constructor(FlashLoanAdapter adapter_) { adapter = adapter_; - token = token_; } - function callFlashLoan() external { - prevAmount = token.balanceOf(address(this)); + function callFlashLoan(IERC20 token, uint256 amount, bytes memory data) external { IERC20[] memory tokens = new IERC20[](1); tokens[0] = token; uint256[] memory amounts = new uint256[](1); - amounts[0] = 1e6; - adapter.flashLoan(address(this), tokens, amounts, ""); + amounts[0] = amount; + adapter.flashLoan(address(this), tokens, amounts, data); } function receiveFlashLoan( IERC20[] memory tokens, uint256[] memory amounts, uint256[] memory fees, - bytes calldata + bytes memory ) external { - assert(token.balanceOf(address(this)) == prevAmount + amounts[0]); - assert(address(tokens[0]) == address(token)); + assert(msg.sender == address(adapter)); + received = tokens[0].balanceOf(address(this)); tokens[0].transfer(address(adapter), amounts[0] + fees[0]); } } diff --git a/test/PriceFeedInverse.t.sol b/test/PriceFeedInverse.t.sol index 3f91ce618..c381b4675 100644 --- a/test/PriceFeedInverse.t.sol +++ b/test/PriceFeedInverse.t.sol @@ -1,28 +1,33 @@ // SPDX-License-Identifier: BUSL-1.1 -pragma solidity ^0.8.17; +pragma solidity ^0.8.17; // solhint-disable-line one-contract-per-file -import { Test } from "forge-std/Test.sol"; +import { Test, stdError } from "forge-std/Test.sol"; import { FixedPointMathLib } from "solady/src/utils/FixedPointMathLib.sol"; import { SafeCastLib } from "solady/src/utils/SafeCastLib.sol"; import { Auditor } from "../contracts/Auditor.sol"; -import { PriceFeedInverse } from "../contracts/PriceFeedInverse.sol"; -import { MockPriceFeed } from "../contracts/mocks/MockPriceFeed.sol"; +import { IAggregatorProxy, PriceFeedInverse } from "../contracts/PriceFeedInverse.sol"; contract PriceFeedInverseTest is Test { using FixedPointMathLib for uint256; PriceFeedInverse internal priceFeedInverse; - MockPriceFeed internal usdArsPriceFeed; + MockAggregatorProxy internal usdArsPriceFeed; function setUp() external { - usdArsPriceFeed = new MockPriceFeed(8, 1_607.5387e8); + usdArsPriceFeed = new MockAggregatorProxy(8, 1, 1_607.5387e8); priceFeedInverse = new PriceFeedInverse(usdArsPriceFeed); } - function test_constructor_setsPriceFeedDecimalsAndBaseUnit() external view { + function test_constructor_setsPriceFeedDecimalsAndBaseUnitSquared() external view { assertEq(address(priceFeedInverse.priceFeed()), address(usdArsPriceFeed)); assertEq(priceFeedInverse.decimals(), 8); - assertEq(priceFeedInverse.baseUnit(), 1e8); + assertEq(priceFeedInverse.baseUnitSquared(), 1e16); + } + + function test_constructor_reverts_whenBaseUnitSquaredOverflows() external { + MockAggregatorProxy priceFeed = new MockAggregatorProxy(39, 1, 1); + vm.expectRevert(stdError.arithmeticError); + new PriceFeedInverse(priceFeed); } function test_latestAnswer_returnsInverse() external { @@ -52,7 +57,7 @@ contract PriceFeedInverseTest is Test { function test_latestAnswer_reverts_whenAnswerIsZero() external { usdArsPriceFeed.setPrice(0); - vm.expectRevert(FixedPointMathLib.MulDivFailed.selector); + vm.expectRevert(FixedPointMathLib.DivFailed.selector); priceFeedInverse.latestAnswer(); } @@ -80,6 +85,95 @@ contract PriceFeedInverseTest is Test { assertEq(auditor.assetPrice(priceFeedInverse), 1e10); } + function test_getRoundData_returnsInverse_withRoundDataUnchanged() external { + usdArsPriceFeed.setRound(1_595.0362e8, 100, 200); + uint80 roundId = usdArsPriceFeed.latestRound(); + + (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = priceFeedInverse + .getRoundData(roundId); + assertEq(id, roundId); + assertEq(answer, 62_695); + assertEq(startedAt, 100); + assertEq(updatedAt, 200); + assertEq(answeredInRound, roundId); + } + + function test_getRoundData_returnsInverse_whenRoundIsHistorical() external { + uint80 roundId = usdArsPriceFeed.latestRound(); + usdArsPriceFeed.setPrice(1_595.0362e8); + + (uint80 id, int256 answer, , , ) = priceFeedInverse.getRoundData(roundId); + assertEq(id, roundId); + assertEq(answer, 62_207); + } + + function test_getRoundData_returnsZero_whenAnswerIsZero() external { + usdArsPriceFeed.setPrice(0); + (, int256 answer, , , ) = priceFeedInverse.getRoundData(usdArsPriceFeed.latestRound()); + assertEq(answer, 0); + } + + function test_getRoundData_returnsZero_whenRoundHasNoData() external { + uint80 roundId = usdArsPriceFeed.latestRound() + 1; + + (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = priceFeedInverse + .getRoundData(roundId); + assertEq(id, roundId); + assertEq(answer, 0); + assertEq(startedAt, 0); + assertEq(updatedAt, 0); + assertEq(answeredInRound, roundId); + } + + function test_getRoundData_reverts_whenAnswerIsNegative() external { + usdArsPriceFeed.setPrice(-1); + uint80 roundId = usdArsPriceFeed.latestRound(); + vm.expectRevert(SafeCastLib.Overflow.selector); + priceFeedInverse.getRoundData(roundId); + } + + function test_latestRoundData_returnsInverse_withRoundDataUnchanged() external { + usdArsPriceFeed.setRound(1_595.0362e8, 100, 200); + + (uint80 id, int256 answer, uint256 startedAt, uint256 updatedAt, uint80 answeredInRound) = priceFeedInverse + .latestRoundData(); + assertEq(id, usdArsPriceFeed.latestRound()); + assertEq(answer, 62_695); + assertEq(startedAt, 100); + assertEq(updatedAt, 200); + assertEq(answeredInRound, usdArsPriceFeed.latestRound()); + } + + function test_latestRoundData_reverts_whenAnswerIsZero() external { + usdArsPriceFeed.setPrice(0); + vm.expectRevert(FixedPointMathLib.DivFailed.selector); + priceFeedInverse.latestRoundData(); + } + + function test_latestRoundData_reverts_whenAnswerIsNegative() external { + usdArsPriceFeed.setPrice(-1); + vm.expectRevert(SafeCastLib.Overflow.selector); + priceFeedInverse.latestRoundData(); + } + + function test_phaseId_returnsSourcePhaseId() external { + assertEq(priceFeedInverse.phaseId(), 1); + + MockAggregatorProxy priceFeed = new MockAggregatorProxy(8, 3, 1); + assertEq(new PriceFeedInverse(priceFeed).phaseId(), 3); + } + + function testFuzz_latestRoundData_matchesLatestAnswer(int256 price) external { + price = bound(price, 1, 2 ** 176 - 1); + usdArsPriceFeed.setPrice(price); + + int256 latestAnswer = priceFeedInverse.latestAnswer(); + (, int256 latestRoundAnswer, , , ) = priceFeedInverse.latestRoundData(); + (, int256 roundAnswer, , , ) = priceFeedInverse.getRoundData(usdArsPriceFeed.latestRound()); + assertEq(latestRoundAnswer, latestAnswer); + assertEq(roundAnswer, latestAnswer); + } + function testFuzz_latestAnswer_returnsOne_whenAnswerIsAboveBaseUnitSquared(int256 price) external { price = bound(price, 1e16, 2 ** 176 - 1); usdArsPriceFeed.setPrice(price); @@ -116,3 +210,44 @@ contract PriceFeedInverseTest is Test { return scale.mulDivUp(1e8, price).mulDivUp(1e8, scale); } } + +contract MockAggregatorProxy is IAggregatorProxy { + uint8 public immutable decimals; + uint16 public immutable phaseId; + uint80 public latestRound; + mapping(uint80 roundId => Round) public rounds; + + constructor(uint8 decimals_, uint16 phaseId_, int256 price_) { + decimals = decimals_; + phaseId = phaseId_; + latestRound = uint80(phaseId_) << 64; + setPrice(price_); + } + + function latestAnswer() external view returns (int256) { + return rounds[latestRound].answer; + } + + function latestRoundData() external view returns (uint80, int256, uint256, uint256, uint80) { + return getRoundData(latestRound); + } + + function getRoundData(uint80 roundId) public view returns (uint80, int256, uint256, uint256, uint80) { + Round memory round = rounds[roundId]; + return (roundId, round.answer, round.startedAt, round.updatedAt, roundId); + } + + function setPrice(int256 price) public { + setRound(price, block.timestamp, block.timestamp); + } + + function setRound(int256 answer, uint256 startedAt, uint256 updatedAt) public { + rounds[++latestRound] = Round(answer, startedAt, updatedAt); + } + + struct Round { + int256 answer; + uint256 startedAt; + uint256 updatedAt; + } +}