From 181b7cfb19ac5161aeada50682ff487453211d0c Mon Sep 17 00:00:00 2001 From: miguelmtzinf Date: Thu, 14 Jul 2022 11:20:17 +0200 Subject: [PATCH 1/3] feat: Initial check of price stability --- .../chainlink/AggregatorV3Interface.sol | 36 +++++++++++++++ contracts/interfaces/IPriceOracleSentinel.sol | 29 +++++++++++- .../oracle/CLAggregators/MockAggregator.sol | 18 ++++++++ .../configuration/PriceOracleSentinel.sol | 46 +++++++++++++++++-- .../libraries/logic/LiquidationLogic.sol | 4 +- .../libraries/logic/ValidationLogic.sol | 10 +++- .../protocol/libraries/types/DataTypes.sol | 2 + 7 files changed, 135 insertions(+), 10 deletions(-) create mode 100644 contracts/dependencies/chainlink/AggregatorV3Interface.sol diff --git a/contracts/dependencies/chainlink/AggregatorV3Interface.sol b/contracts/dependencies/chainlink/AggregatorV3Interface.sol new file mode 100644 index 000000000..cc2587dc7 --- /dev/null +++ b/contracts/dependencies/chainlink/AggregatorV3Interface.sol @@ -0,0 +1,36 @@ +// SPDX-License-Identifier: MIT +// Chainlink Contracts v0.8 +pragma solidity ^0.8.0; + +interface AggregatorV3Interface { + function decimals() external view returns (uint8); + + function description() external view returns (string memory); + + function version() external view returns (uint256); + + // getRoundData and latestRoundData should both raise "No data present" + // if they do not have data to report, instead of returning unset values + // which could be misinterpreted as actual reported values. + function getRoundData(uint80 _roundId) + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ); +} diff --git a/contracts/interfaces/IPriceOracleSentinel.sol b/contracts/interfaces/IPriceOracleSentinel.sol index 6d05bf0f9..6c4429b6c 100644 --- a/contracts/interfaces/IPriceOracleSentinel.sol +++ b/contracts/interfaces/IPriceOracleSentinel.sol @@ -21,6 +21,12 @@ interface IPriceOracleSentinel { */ event GracePeriodUpdated(uint256 newGracePeriod); + /** + * @dev Emitted after the price expiration time is updated + * @param newPriceExpirationTime The new price expiration time + */ + event PriceExpirationTimeUpdated(uint256 newPriceExpirationTime); + /** * @notice Returns the PoolAddressesProvider * @return The address of the PoolAddressesProvider contract @@ -30,16 +36,23 @@ interface IPriceOracleSentinel { /** * @notice Returns true if the `borrow` operation is allowed. * @dev Operation not allowed when PriceOracle is down or grace period not passed. + * @param priceOracle The address of the price oracle + * @param asset The address of the asset to borrow * @return True if the `borrow` operation is allowed, false otherwise. */ - function isBorrowAllowed() external view returns (bool); + function isBorrowAllowed(address priceOracle, address asset) external view returns (bool); /** * @notice Returns true if the `liquidation` operation is allowed. * @dev Operation not allowed when PriceOracle is down or grace period not passed. + * @param priceOracle The address of the price oracle + * @param debtAsset The address of the debt asset to liquidate * @return True if the `liquidation` operation is allowed, false otherwise. */ - function isLiquidationAllowed() external view returns (bool); + function isLiquidationAllowed(address priceOracle, address debtAsset) + external + view + returns (bool); /** * @notice Updates the address of the sequencer oracle @@ -53,6 +66,12 @@ interface IPriceOracleSentinel { */ function setGracePeriod(uint256 newGracePeriod) external; + /** + * @notice Updates the price expiration time + * @param newPriceExpirationTime The value of the new price expiration time + */ + function setPriceExpirationTime(uint256 newPriceExpirationTime) external; + /** * @notice Returns the SequencerOracle * @return The address of the sequencer oracle contract @@ -64,4 +83,10 @@ interface IPriceOracleSentinel { * @return The duration of the grace period */ function getGracePeriod() external view returns (uint256); + + /** + * @notice Returns the price expiration time + * @return The duration after the price of assets can be considered as expired or stale (in seconds) + */ + function getPriceExpirationTime() external view returns (uint256); } diff --git a/contracts/mocks/oracle/CLAggregators/MockAggregator.sol b/contracts/mocks/oracle/CLAggregators/MockAggregator.sol index e21b54629..4fb5985b5 100644 --- a/contracts/mocks/oracle/CLAggregators/MockAggregator.sol +++ b/contracts/mocks/oracle/CLAggregators/MockAggregator.sol @@ -3,11 +3,15 @@ pragma solidity 0.8.10; contract MockAggregator { int256 private _latestAnswer; + uint80 private _roundId; + uint256 private _startedAt; + uint256 private _updatedAt; event AnswerUpdated(int256 indexed current, uint256 indexed roundId, uint256 updatedAt); constructor(int256 initialAnswer) { _latestAnswer = initialAnswer; + _startedAt = block.timestamp; emit AnswerUpdated(initialAnswer, 0, block.timestamp); } @@ -22,4 +26,18 @@ contract MockAggregator { function decimals() external pure returns (uint8) { return 8; } + + function latestRoundData() + external + view + returns ( + uint80 roundId, + int256 answer, + uint256 startedAt, + uint256 updatedAt, + uint80 answeredInRound + ) + { + return (_roundId, _latestAnswer, _startedAt, block.timestamp, _roundId); + } } diff --git a/contracts/protocol/configuration/PriceOracleSentinel.sol b/contracts/protocol/configuration/PriceOracleSentinel.sol index 81b22d8db..4ad41a090 100644 --- a/contracts/protocol/configuration/PriceOracleSentinel.sol +++ b/contracts/protocol/configuration/PriceOracleSentinel.sol @@ -1,11 +1,13 @@ // SPDX-License-Identifier: BUSL-1.1 pragma solidity 0.8.10; +import {AggregatorV3Interface} from '../../dependencies/chainlink/AggregatorV3Interface.sol'; import {Errors} from '../libraries/helpers/Errors.sol'; import {IPoolAddressesProvider} from '../../interfaces/IPoolAddressesProvider.sol'; import {IPriceOracleSentinel} from '../../interfaces/IPriceOracleSentinel.sol'; import {ISequencerOracle} from '../../interfaces/ISequencerOracle.sol'; import {IACLManager} from '../../interfaces/IACLManager.sol'; +import {IAaveOracle} from '../../interfaces/IAaveOracle.sol'; /** * @title PriceOracleSentinel @@ -42,30 +44,40 @@ contract PriceOracleSentinel is IPriceOracleSentinel { uint256 internal _gracePeriod; + uint256 internal _priceExpirationTime; + /** * @dev Constructor * @param provider The address of the PoolAddressesProvider * @param oracle The address of the SequencerOracle * @param gracePeriod The duration of the grace period in seconds + * @param priceExpirationTime The expiration time of asset prices in seconds */ constructor( IPoolAddressesProvider provider, ISequencerOracle oracle, - uint256 gracePeriod + uint256 gracePeriod, + uint256 priceExpirationTime ) { ADDRESSES_PROVIDER = provider; _sequencerOracle = oracle; _gracePeriod = gracePeriod; + _priceExpirationTime = priceExpirationTime; } /// @inheritdoc IPriceOracleSentinel - function isBorrowAllowed() public view override returns (bool) { - return _isUpAndGracePeriodPassed(); + function isBorrowAllowed(address priceOracle, address asset) public view override returns (bool) { + return _isUpAndGracePeriodPassed() && !_isPriceStale(priceOracle, asset); } /// @inheritdoc IPriceOracleSentinel - function isLiquidationAllowed() public view override returns (bool) { - return _isUpAndGracePeriodPassed(); + function isLiquidationAllowed(address priceOracle, address debtAsset) + public + view + override + returns (bool) + { + return _isUpAndGracePeriodPassed() && !_isPriceStale(priceOracle, debtAsset); } /** @@ -77,6 +89,19 @@ contract PriceOracleSentinel is IPriceOracleSentinel { return answer == 0 && block.timestamp - lastUpdateTimestamp > _gracePeriod; } + /** + * @notice Checks the price of the asset is not stale. It can be considered stale if the time passed since the last + * is longer than the price expiration time. + * @param priceOracle The address of the price oracle + * @param asset The address of the asset to check its price status + * @return True if the price is stale, false otherwise + */ + function _isPriceStale(address priceOracle, address asset) internal view returns (bool) { + address source = IAaveOracle(priceOracle).getSourceOfAsset(asset); + (, , , uint256 updatedAt, ) = AggregatorV3Interface(source).latestRoundData(); + return (block.timestamp - updatedAt) > _priceExpirationTime; + } + /// @inheritdoc IPriceOracleSentinel function setSequencerOracle(address newSequencerOracle) public onlyPoolAdmin { _sequencerOracle = ISequencerOracle(newSequencerOracle); @@ -89,6 +114,12 @@ contract PriceOracleSentinel is IPriceOracleSentinel { emit GracePeriodUpdated(newGracePeriod); } + /// @inheritdoc IPriceOracleSentinel + function setPriceExpirationTime(uint256 newPriceExpirationTime) public onlyRiskOrPoolAdmins { + _priceExpirationTime = newPriceExpirationTime; + emit PriceExpirationTimeUpdated(newPriceExpirationTime); + } + /// @inheritdoc IPriceOracleSentinel function getSequencerOracle() public view returns (address) { return address(_sequencerOracle); @@ -98,4 +129,9 @@ contract PriceOracleSentinel is IPriceOracleSentinel { function getGracePeriod() public view returns (uint256) { return _gracePeriod; } + + /// @inheritdoc IPriceOracleSentinel + function getPriceExpirationTime() public view returns (uint256) { + return _priceExpirationTime; + } } diff --git a/contracts/protocol/libraries/logic/LiquidationLogic.sol b/contracts/protocol/libraries/logic/LiquidationLogic.sol index 7d91d3365..133a5afcd 100644 --- a/contracts/protocol/libraries/logic/LiquidationLogic.sol +++ b/contracts/protocol/libraries/logic/LiquidationLogic.sol @@ -132,9 +132,11 @@ library LiquidationLogic { collateralReserve, DataTypes.ValidateLiquidationCallParams({ debtReserveCache: vars.debtReserveCache, + debtAsset: params.debtAsset, totalDebt: vars.userTotalDebt, healthFactor: vars.healthFactor, - priceOracleSentinel: params.priceOracleSentinel + priceOracleSentinel: params.priceOracleSentinel, + oracle: params.priceOracle }) ); diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index fb2eda0c1..c06c230c1 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -152,7 +152,10 @@ library ValidationLogic { require( params.priceOracleSentinel == address(0) || - IPriceOracleSentinel(params.priceOracleSentinel).isBorrowAllowed(), + IPriceOracleSentinel(params.priceOracleSentinel).isBorrowAllowed( + params.oracle, + params.asset + ), Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED ); @@ -521,7 +524,10 @@ library ValidationLogic { require( params.priceOracleSentinel == address(0) || params.healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || - IPriceOracleSentinel(params.priceOracleSentinel).isLiquidationAllowed(), + IPriceOracleSentinel(params.priceOracleSentinel).isLiquidationAllowed( + params.debtAsset, + params.oracle + ), Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED ); diff --git a/contracts/protocol/libraries/types/DataTypes.sol b/contracts/protocol/libraries/types/DataTypes.sol index 7113a0a51..180c523d7 100644 --- a/contracts/protocol/libraries/types/DataTypes.sol +++ b/contracts/protocol/libraries/types/DataTypes.sol @@ -239,9 +239,11 @@ library DataTypes { struct ValidateLiquidationCallParams { ReserveCache debtReserveCache; + address debtAsset; uint256 totalDebt; uint256 healthFactor; address priceOracleSentinel; + address oracle; } struct CalculateInterestRatesParams { From 58f3a145c5eb5986a7d87d2a0ce2d157702d844a Mon Sep 17 00:00:00 2001 From: miguelmtzinf Date: Fri, 15 Jul 2022 12:19:36 +0200 Subject: [PATCH 2/3] fix: Fix isLiqAllowed validation --- contracts/protocol/libraries/logic/ValidationLogic.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/contracts/protocol/libraries/logic/ValidationLogic.sol b/contracts/protocol/libraries/logic/ValidationLogic.sol index c06c230c1..f3f5cb101 100644 --- a/contracts/protocol/libraries/logic/ValidationLogic.sol +++ b/contracts/protocol/libraries/logic/ValidationLogic.sol @@ -525,8 +525,8 @@ library ValidationLogic { params.priceOracleSentinel == address(0) || params.healthFactor < MINIMUM_HEALTH_FACTOR_LIQUIDATION_THRESHOLD || IPriceOracleSentinel(params.priceOracleSentinel).isLiquidationAllowed( - params.debtAsset, - params.oracle + params.oracle, + params.debtAsset ), Errors.PRICE_ORACLE_SENTINEL_CHECK_FAILED ); From 94e02e998676905cf76e2db65b08e568b2b618ed Mon Sep 17 00:00:00 2001 From: miguelmtzinf Date: Fri, 15 Jul 2022 12:19:56 +0200 Subject: [PATCH 3/3] test: Update tests of price oracle sentinel --- .../oracle/CLAggregators/MockAggregator.sol | 12 +- test-suites/price-oracle-sentinel.spec.ts | 248 ++++++++++++++---- 2 files changed, 205 insertions(+), 55 deletions(-) diff --git a/contracts/mocks/oracle/CLAggregators/MockAggregator.sol b/contracts/mocks/oracle/CLAggregators/MockAggregator.sol index 4fb5985b5..19942d58b 100644 --- a/contracts/mocks/oracle/CLAggregators/MockAggregator.sol +++ b/contracts/mocks/oracle/CLAggregators/MockAggregator.sol @@ -38,6 +38,16 @@ contract MockAggregator { uint80 answeredInRound ) { - return (_roundId, _latestAnswer, _startedAt, block.timestamp, _roundId); + return ( + _roundId, + _latestAnswer, + _startedAt, + _updatedAt == 0 ? block.timestamp : _updatedAt, + _roundId + ); + } + + function setLastUpdateTimestamp(uint256 updatedAt) external { + _updatedAt = updatedAt; } } diff --git a/test-suites/price-oracle-sentinel.spec.ts b/test-suites/price-oracle-sentinel.spec.ts index 93c3bf6e1..08591bcbb 100644 --- a/test-suites/price-oracle-sentinel.spec.ts +++ b/test-suites/price-oracle-sentinel.spec.ts @@ -1,25 +1,38 @@ import { expect } from 'chai'; -import { BigNumber, utils } from 'ethers'; +import { BigNumber, BigNumberish, utils } from 'ethers'; import { timeLatest } from '../helpers/misc-utils'; import { MAX_UINT_AMOUNT, ZERO_ADDRESS } from '../helpers/constants'; import { ProtocolErrors, RateMode } from '../helpers/types'; import { + AaveOracle, + MockAggregator__factory, PriceOracleSentinel, PriceOracleSentinel__factory, SequencerOracle, SequencerOracle__factory, } from '../types'; import { getFirstSigner } from '@aave/deploy-v3/dist/helpers/utilities/signer'; -import { makeSuite, TestEnv } from './helpers/make-suite'; +import { makeSuite, SignerWithAddress, TestEnv } from './helpers/make-suite'; import { convertToCurrencyDecimals } from '../helpers/contracts-helpers'; import { calcExpectedVariableDebtTokenBalance } from './helpers/utils/calculations'; import { getReserveData, getUserData } from './helpers/utils/helpers'; -import './helpers/utils/wadraymath'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; -import { waitForTx, increaseTime } from '@aave/deploy-v3'; +import { waitForTx, increaseTime, evmSnapshot, evmRevert } from '@aave/deploy-v3'; +import './helpers/utils/wadraymath'; declare var hre: HardhatRuntimeEnvironment; +const setPriceWithMockAggregator = async ( + poolAdmin: SignerWithAddress, + aaveOracle: AaveOracle, + asset: string, + price: BigNumberish +) => { + const oracle = await new MockAggregator__factory(poolAdmin.signer).deploy(price); + await aaveOracle.connect(poolAdmin.signer).setAssetSources([asset], [oracle.address]); + return oracle; +}; + makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { const { PRICE_ORACLE_SENTINEL_CHECK_FAILED, @@ -31,10 +44,11 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { let sequencerOracle: SequencerOracle; let priceOracleSentinel: PriceOracleSentinel; - const GRACE_PERIOD = BigNumber.from(60 * 60); + const GRACE_PERIOD = BigNumber.from(60 * 60); // 1h + const PRICE_EXPIRATION_TIME = BigNumber.from(10 * 60); // 10 min before(async () => { - const { addressesProvider, deployer, oracle } = testEnv; + const { addressesProvider, deployer } = testEnv; // Deploy SequencerOracle sequencerOracle = await ( @@ -45,19 +59,13 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { await new PriceOracleSentinel__factory(await getFirstSigner()).deploy( addressesProvider.address, sequencerOracle.address, - GRACE_PERIOD + GRACE_PERIOD, + PRICE_EXPIRATION_TIME ) ).deployed(); - - await waitForTx(await addressesProvider.setPriceOracle(oracle.address)); - }); - - after(async () => { - const { aaveOracle, addressesProvider } = testEnv; - await waitForTx(await addressesProvider.setPriceOracle(aaveOracle.address)); }); - it('Admin sets a PriceOracleSentinel and activate it for DAI and WETH', async () => { + it.only('Admin sets a PriceOracleSentinel and activate it for DAI and WETH', async () => { const { addressesProvider, poolAdmin } = testEnv; expect( @@ -75,7 +83,7 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { expect(answer[3]).to.be.eq(0); }); - it('Pooladmin updates grace period for sentinel', async () => { + it.only('PoolAdmin updates grace period for sentinel', async () => { const { poolAdmin } = testEnv; const newGracePeriod = 0; @@ -87,7 +95,7 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { expect(await priceOracleSentinel.getGracePeriod()).to.be.eq(newGracePeriod); }); - it('Risk admin updates grace period for sentinel', async () => { + it.only('Risk admin updates grace period for sentinel', async () => { const { riskAdmin } = testEnv; expect(await priceOracleSentinel.getGracePeriod()).to.be.eq(0); @@ -97,7 +105,7 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { expect(await priceOracleSentinel.getGracePeriod()).to.be.eq(GRACE_PERIOD); }); - it('User tries to set grace period for sentinel', async () => { + it.only('User tries to set grace period for sentinel (revert expected)', async () => { const { users: [user], } = testEnv; @@ -109,7 +117,45 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { expect(await priceOracleSentinel.getGracePeriod()).to.not.be.eq(0); }); - it('Pooladmin update the sequencer oracle', async () => { + it.only('PoolAdmin updates price expiration time for sentinel', async () => { + const { poolAdmin } = testEnv; + + const newGracePeriod = 0; + + expect(await priceOracleSentinel.getPriceExpirationTime()).to.be.eq(PRICE_EXPIRATION_TIME); + expect(await priceOracleSentinel.connect(poolAdmin.signer).setPriceExpirationTime(0)) + .to.emit(priceOracleSentinel, 'PriceExpirationTimeUpdated') + .withArgs(0); + expect(await priceOracleSentinel.getPriceExpirationTime()).to.be.eq(newGracePeriod); + }); + + it.only('Risk admin updates price expiration time for sentinel', async () => { + const { riskAdmin } = testEnv; + + expect(await priceOracleSentinel.getPriceExpirationTime()).to.be.eq(0); + expect( + await priceOracleSentinel + .connect(riskAdmin.signer) + .setPriceExpirationTime(PRICE_EXPIRATION_TIME) + ) + .to.emit(priceOracleSentinel, 'PriceExpirationTimeUpdated') + .withArgs(PRICE_EXPIRATION_TIME); + expect(await priceOracleSentinel.getPriceExpirationTime()).to.be.eq(PRICE_EXPIRATION_TIME); + }); + + it.only('User tries to set price expiration time for sentinel (revert expected)', async () => { + const { + users: [user], + } = testEnv; + + expect(await priceOracleSentinel.getPriceExpirationTime()).to.be.eq(PRICE_EXPIRATION_TIME); + await expect( + priceOracleSentinel.connect(user.signer).setPriceExpirationTime(0) + ).to.be.revertedWith(CALLER_NOT_RISK_OR_POOL_ADMIN); + expect(await priceOracleSentinel.getPriceExpirationTime()).to.not.be.eq(0); + }); + + it.only('PoolAdmin updates the sequencer oracle', async () => { const { poolAdmin } = testEnv; const newSequencerOracle = ZERO_ADDRESS; @@ -132,7 +178,7 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { expect(await priceOracleSentinel.getSequencerOracle()).to.be.eq(sequencerOracle.address); }); - it('User tries to update sequencer oracle', async () => { + it.only('User tries to update sequencer oracle (revert expected)', async () => { const { users: [user], } = testEnv; @@ -145,13 +191,13 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { expect(await priceOracleSentinel.getSequencerOracle()).to.be.eq(sequencerOracle.address); }); - it('Borrow DAI', async () => { + it.only('User borrow DAI', async () => { const { dai, weth, users: [depositor, borrower, borrower2], pool, - oracle, + aaveOracle, } = testEnv; //mints DAI to depositor @@ -186,7 +232,7 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { //user 2 borrows const userGlobalData = await pool.getUserAccountData(currBorrower.address); - const daiPrice = await oracle.getAssetPrice(dai.address); + const daiPrice = await aaveOracle.getAssetPrice(dai.address); const amountDAIToBorrow = await convertToCurrencyDecimals( dai.address, @@ -199,16 +245,23 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { } }); - it('Kill sequencer and drop health factor below 1', async () => { + it.only('Kill sequencer and drop health factor below 1', async () => { const { dai, + poolAdmin, users: [, borrower], pool, - oracle, + aaveOracle, } = testEnv; - const daiPrice = await oracle.getAssetPrice(dai.address); - await oracle.setAssetPrice(dai.address, daiPrice.percentMul(11000)); + const daiPrice = await aaveOracle.getAssetPrice(dai.address); + await setPriceWithMockAggregator( + poolAdmin, + aaveOracle, + dai.address, + daiPrice.percentMul(11000) + ); + const userGlobalData = await pool.getUserAccountData(borrower.address); expect(userGlobalData.healthFactor).to.be.lt(utils.parseUnits('1', 18), INVALID_HF); @@ -216,7 +269,7 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { waitForTx(await sequencerOracle.setAnswer(true, currAnswer[3])); }); - it('Tries to liquidate borrower when sequencer is down (HF > 0.95) (revert expected)', async () => { + it.only('Tries to liquidate borrower when sequencer is down (HF > 0.95) (revert expected)', async () => { const { pool, dai, @@ -241,28 +294,34 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { ).to.be.revertedWith(PRICE_ORACLE_SENTINEL_CHECK_FAILED); }); - it('Drop health factor lower', async () => { + it.only('Drop health factor lower', async () => { const { dai, + poolAdmin, users: [, borrower], pool, - oracle, + aaveOracle, } = testEnv; - const daiPrice = await oracle.getAssetPrice(dai.address); - await oracle.setAssetPrice(dai.address, daiPrice.percentMul(11000)); + const daiPrice = await aaveOracle.getAssetPrice(dai.address); + await setPriceWithMockAggregator( + poolAdmin, + aaveOracle, + dai.address, + daiPrice.percentMul(11000) + ); const userGlobalData = await pool.getUserAccountData(borrower.address); expect(userGlobalData.healthFactor).to.be.lt(utils.parseUnits('1', 18), INVALID_HF); }); - it('Liquidates borrower when sequencer is down (HF < 0.95)', async () => { + it.only('Liquidates borrower when sequencer is down (HF < 0.95)', async () => { const { pool, dai, weth, users: [, borrower], - oracle, + aaveOracle, helpersContract, deployer, } = testEnv; @@ -310,8 +369,8 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { const daiReserveDataAfter = await getReserveData(helpersContract, dai.address); const ethReserveDataAfter = await getReserveData(helpersContract, weth.address); - const collateralPrice = await oracle.getAssetPrice(weth.address); - const principalPrice = await oracle.getAssetPrice(dai.address); + const collateralPrice = await aaveOracle.getAssetPrice(weth.address); + const principalPrice = await aaveOracle.getAssetPrice(dai.address); const collateralDecimals = (await helpersContract.getReserveConfigurationData(weth.address)) .decimals; @@ -383,13 +442,12 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { ).to.be.true; }); - it('User tries to borrow (revert expected)', async () => { + it.only('User tries to borrow (revert expected)', async () => { const { dai, weth, users: [, , , user], pool, - oracle, } = testEnv; await weth.connect(user.signer)['mint(uint256)'](utils.parseUnits('0.06775', 18)); @@ -405,11 +463,11 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { ).to.be.revertedWith(PRICE_ORACLE_SENTINEL_CHECK_FAILED); }); - it('Turn on sequencer', async () => { + it.only('Turn on sequencer', async () => { await waitForTx(await sequencerOracle.setAnswer(false, await timeLatest())); }); - it('User tries to borrow (revert expected)', async () => { + it.only('User tries to borrow (revert expected)', async () => { const { dai, weth, @@ -430,13 +488,13 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { ).to.be.revertedWith(PRICE_ORACLE_SENTINEL_CHECK_FAILED); }); - it('Turn off sequencer + increase time more than grace period', async () => { + it.only('Turn off sequencer + increase time more than grace period', async () => { const currAnswer = await sequencerOracle.latestRoundData(); await waitForTx(await sequencerOracle.setAnswer(true, currAnswer[3])); await increaseTime(GRACE_PERIOD.mul(2).toNumber()); }); - it('User tries to borrow (revert expected)', async () => { + it.only('User tries to borrow (revert expected)', async () => { const { dai, weth, @@ -457,12 +515,51 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { ).to.be.revertedWith(PRICE_ORACLE_SENTINEL_CHECK_FAILED); }); - it('Turn on sequencer + increase time past grace period', async () => { + it.only('Turn on sequencer + increase time past grace period', async () => { await waitForTx(await sequencerOracle.setAnswer(false, await timeLatest())); await increaseTime(GRACE_PERIOD.mul(2).toNumber()); }); - it('User tries to borrow', async () => { + it.only('User tries to borrow DAI while its price is stale (revert expected)', async () => { + const { + dai, + weth, + users: [, , , user], + pool, + aaveOracle, + } = testEnv; + + const snapId = await evmSnapshot(); + + const oracleSourceAddress = await aaveOracle.getSourceOfAsset(dai.address); + const oracleSource = MockAggregator__factory.connect(oracleSourceAddress, user.signer); + const { updatedAt: updatedAtBefore } = await oracleSource.latestRoundData(); + + // Mock the last update of DAI price + const newUpdatedAt = (await timeLatest()).sub(PRICE_EXPIRATION_TIME); + await oracleSource.setLastUpdateTimestamp(newUpdatedAt); + const { updatedAt: updatedAtAfter } = await oracleSource.latestRoundData(); + expect(updatedAtAfter).to.be.not.eq(updatedAtBefore); + expect(updatedAtAfter).to.be.eq(newUpdatedAt); + + expect(await priceOracleSentinel.isBorrowAllowed(aaveOracle.address, dai.address)).to.be.false; + + await weth.connect(user.signer)['mint(uint256)'](utils.parseUnits('0.06775', 18)); + await weth.connect(user.signer).approve(pool.address, MAX_UINT_AMOUNT); + await pool + .connect(user.signer) + .supply(weth.address, utils.parseUnits('0.06775', 18), user.address, 0); + + await expect( + pool + .connect(user.signer) + .borrow(dai.address, utils.parseUnits('100', 18), RateMode.Variable, 0, user.address) + ).to.be.revertedWith(PRICE_ORACLE_SENTINEL_CHECK_FAILED); + + await evmRevert(snapId); + }); + + it.only('User borrows more DAI', async () => { const { dai, weth, @@ -483,28 +580,73 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { ); }); - it('Increase health factor', async () => { + it.only('Increase health factor', async () => { const { + poolAdmin, dai, users: [, borrower], pool, - oracle, + aaveOracle, } = testEnv; - const daiPrice = await oracle.getAssetPrice(dai.address); - await oracle.setAssetPrice(dai.address, daiPrice.percentMul(9500)); + + const daiPrice = await aaveOracle.getAssetPrice(dai.address); + await setPriceWithMockAggregator(poolAdmin, aaveOracle, dai.address, daiPrice.percentMul(9500)); const userGlobalData = await pool.getUserAccountData(borrower.address); expect(userGlobalData.healthFactor).to.be.lt(utils.parseUnits('1', 18), INVALID_HF); expect(userGlobalData.healthFactor).to.be.gt(utils.parseUnits('0.95', 18), INVALID_HF); }); - it('Liquidates borrower when sequencer is up again', async () => { + it.only('Tries to liquidate borrower when debt price is stale (revert expected)', async () => { + const { + pool, + dai, + weth, + users: [, borrower, , user], + helpersContract, + aaveOracle, + } = testEnv; + + const snapId = await evmSnapshot(); + const oracleSourceAddress = await aaveOracle.getSourceOfAsset(dai.address); + const oracleSource = MockAggregator__factory.connect(oracleSourceAddress, user.signer); + const { updatedAt: updatedAtBefore } = await oracleSource.latestRoundData(); + + // Mock the last update of DAI price + const newUpdatedAt = (await timeLatest()).sub(PRICE_EXPIRATION_TIME); + await oracleSource.setLastUpdateTimestamp(newUpdatedAt); + const { updatedAt: updatedAtAfter } = await oracleSource.latestRoundData(); + expect(updatedAtAfter).to.be.not.eq(updatedAtBefore); + expect(updatedAtAfter).to.be.eq(newUpdatedAt); + + expect(await priceOracleSentinel.isLiquidationAllowed(aaveOracle.address, dai.address)).to.be + .false; + + await dai['mint(uint256)'](await convertToCurrencyDecimals(dai.address, '1000')); + await dai.approve(pool.address, MAX_UINT_AMOUNT); + + const userReserveDataBefore = await getUserData( + pool, + helpersContract, + dai.address, + borrower.address + ); + + const amountToLiquidate = userReserveDataBefore.currentVariableDebt.div(2); + await expect( + pool.liquidationCall(weth.address, dai.address, borrower.address, amountToLiquidate, true) + ).to.be.revertedWith(PRICE_ORACLE_SENTINEL_CHECK_FAILED); + + await evmRevert(snapId); + }); + + it.only('Liquidates borrower when sequencer is up again', async () => { const { pool, dai, weth, users: [, , borrower], - oracle, + aaveOracle, helpersContract, deployer, } = testEnv; @@ -550,13 +692,11 @@ makeSuite('PriceOracleSentinel', (testEnv: TestEnv) => { borrower.address ); - const userGlobalDataAfter = await pool.getUserAccountData(borrower.address); - const daiReserveDataAfter = await getReserveData(helpersContract, dai.address); const ethReserveDataAfter = await getReserveData(helpersContract, weth.address); - const collateralPrice = await oracle.getAssetPrice(weth.address); - const principalPrice = await oracle.getAssetPrice(dai.address); + const collateralPrice = await aaveOracle.getAssetPrice(weth.address); + const principalPrice = await aaveOracle.getAssetPrice(dai.address); const collateralDecimals = (await helpersContract.getReserveConfigurationData(weth.address)) .decimals;