diff --git a/README.md b/README.md index 894fc0a..4cafbaf 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ [foundry]: https://getfoundry.sh/ [foundry-badge]: https://img.shields.io/badge/Built%20with-Foundry-FFDB1C.svg -This module registers pegged asset oracles and will trigger a lockdown mode for SparkLend if certain price thresholds are met. For example, if WBTC/BTC is observed to reach a market price of 0.95 and the threshold for this oracle is set to 0.95 then anyone can permissionlessly trigger to set SparkLend into lockdown mode in which all collateral assets have their LTVs set to 0 and all borrowable assets are frozen. +This module registers pegged asset oracles and will trigger a lockdown mode for SparkLend if certain price thresholds are met. For example, if WBTC/BTC is observed to reach a market price of 0.95 and the threshold for this oracle is set to 0.95 then anyone can permissionlessly trigger to set SparkLend into lockdown mode which prevents new borrows on all assets. The reasoning behind this is to limit the damage in the event of extreme market conditions. Depegging assets may be temporary, but there is no harm in an excess of caution in these situations. Users can still top up collateral and repay/withdraw in lockdown mode. This just prevents further borrowing to limit downside exposure to lenders. diff --git a/src/KillSwitchOracle.sol b/src/KillSwitchOracle.sol index 90b13d0..d34a37b 100644 --- a/src/KillSwitchOracle.sol +++ b/src/KillSwitchOracle.sol @@ -118,23 +118,10 @@ contract KillSwitchOracle is IKillSwitchOracle, Ownable { config.getPaused() ) continue; - if (config.getLtv() > 0) { - // This asset is being used as collateral - // We only want to disable new borrows against this to allow users - // to top up their positions to prevent getting liquidated - poolConfigurator.configureReserveAsCollateral( - asset, - 0, - config.getLiquidationThreshold(), - config.getLiquidationBonus() - ); - - emit AssetLTV0(asset); - } else if (config.getBorrowingEnabled()) { - // This is a borrow-only asset - poolConfigurator.setReserveFreeze(asset, true); - - emit AssetFrozen(asset); + if (config.getBorrowingEnabled()) { + poolConfigurator.setReserveBorrowing(asset, false); + + emit BorrowDisabled(asset); } } } diff --git a/src/interfaces/IKillSwitchOracle.sol b/src/interfaces/IKillSwitchOracle.sol index 7535536..5deb977 100644 --- a/src/interfaces/IKillSwitchOracle.sol +++ b/src/interfaces/IKillSwitchOracle.sol @@ -32,16 +32,10 @@ interface IKillSwitchOracle { event Trigger(address indexed oracle, uint256 threshold, uint256 price); /** - * @dev Emitted when the LTV (Loan to Value) of an asset is set to 0. - * @param asset The address of the asset whose LTV is set to 0. + * @dev Emitted when the borrow is disabled for an asset. + * @param asset The address of the asset whose borrow has been disabled. */ - event AssetLTV0(address indexed asset); - - /** - * @dev Emitted when an asset is frozen. - * @param asset The address of the asset that is frozen. - */ - event AssetFrozen(address indexed asset); + event BorrowDisabled(address indexed asset); /** * @dev Emitted when the contract is reset. @@ -138,9 +132,8 @@ interface IKillSwitchOracle { * @notice Permissionless function to trigger the kill switch. * @dev If the kill switch has not been triggered, the oracle threshold has been defined, * and the oracle is below the threshold, the kill switch is triggered. This will - * set LTV to 0 on any collateral asset preventing new loans and freeze any freeze - * any other asset which can be borrowed. This function can only be called once - * and will require a call to `reset()` by the owner to be called again. + * disable borrowing on all assets. This function can only be called once and will + * require a call to `reset()` by the owner to be called again. * @param oracle The address of the oracle which is below the threshold. */ function trigger(address oracle) external; diff --git a/test/KillSwitchOracle.t.sol b/test/KillSwitchOracle.t.sol index 79674e0..1de2b2c 100644 --- a/test/KillSwitchOracle.t.sol +++ b/test/KillSwitchOracle.t.sol @@ -19,8 +19,7 @@ contract KillSwitchOracleTestBase is Test { event SetOracle(address indexed oracle, uint256 threshold); event DisableOracle(address indexed oracle); event Trigger(address indexed oracle, uint256 threshold, uint256 price); - event AssetLTV0(address indexed asset); - event AssetFrozen(address indexed asset); + event BorrowDisabled(address indexed asset); event Reset(); MockPoolAddressesProvider poolAddressesProvider; @@ -40,7 +39,6 @@ contract KillSwitchOracleTestBase is Test { address asset3 = makeAddr("asset3"); address asset4 = makeAddr("asset4"); address asset5 = makeAddr("asset5"); - address asset6 = makeAddr("asset6"); function setUp() public { pool = new MockPool(); @@ -262,9 +260,6 @@ contract KillSwitchOracleTriggerTests is KillSwitchOracleTestBase { bool frozen; bool paused; bool borrowingEnabled; - uint256 ltv; - uint256 liquidationThreshold; - uint256 liquidationBonus; } using ReserveConfiguration for DataTypes.ReserveConfigurationMap; @@ -312,72 +307,46 @@ contract KillSwitchOracleTriggerTests is KillSwitchOracleTestBase { } function test_trigger() public { - ReserveConfigParams[6] memory reserves = [ - // Collateral asset /w borrow enabled (Ex. ETH, wstETH) + ReserveConfigParams[5] memory reserves = [ + // Asset with borrow enabled (Ex. ETH, wstETH, DAI) ReserveConfigParams({ asset: asset1, active: true, frozen: false, paused: false, - borrowingEnabled: true, - ltv: 80_00, - liquidationThreshold: 83_00, - liquidationBonus: 105_00 + borrowingEnabled: true }), - // Collateral asset /w no borrow (Ex. sDAI) + // Collateral-only asset (Ex. sDAI) ReserveConfigParams({ asset: asset2, active: true, frozen: false, paused: false, - borrowingEnabled: false, - ltv: 80_00, - liquidationThreshold: 83_00, - liquidationBonus: 105_00 + borrowingEnabled: false }), - // Borrow-only asset (Ex. DAI, USDC) + // Frozen asset (Ex. GNO) ReserveConfigParams({ asset: asset3, active: true, - frozen: false, - paused: false, - borrowingEnabled: true, - ltv: 0, - liquidationThreshold: 0, - liquidationBonus: 0 - }), - // Frozen/LTV0 asset (Ex. GNO) - ReserveConfigParams({ - asset: asset4, - active: true, frozen: true, paused: false, - borrowingEnabled: true, - ltv: 0, - liquidationThreshold: 25_00, - liquidationBonus: 110_00 + borrowingEnabled: true }), // Paused asset ReserveConfigParams({ - asset: asset5, + asset: asset4, active: true, frozen: false, paused: true, - borrowingEnabled: true, - ltv: 80_00, - liquidationThreshold: 83_00, - liquidationBonus: 105_00 + borrowingEnabled: true }), // Inactive asset ReserveConfigParams({ - asset: asset6, + asset: asset5, active: false, frozen: false, paused: false, - borrowingEnabled: false, - ltv: 0, - liquidationThreshold: 0, - liquidationBonus: 0 + borrowingEnabled: false }) ]; @@ -385,7 +354,7 @@ contract KillSwitchOracleTriggerTests is KillSwitchOracleTestBase { _initReserve(reserves[i]); } - assertEq(pool.getReservesList().length, 6); + assertEq(pool.getReservesList().length, 5); vm.prank(owner); killSwitchOracle.setOracle(address(oracle1), 0.99e8); @@ -394,18 +363,12 @@ contract KillSwitchOracleTriggerTests is KillSwitchOracleTestBase { vm.expectEmit(address(killSwitchOracle)); emit Trigger(address(oracle1), 0.99e8, 0.98e8); vm.expectEmit(address(killSwitchOracle)); - emit AssetLTV0(asset1); - vm.expectEmit(address(killSwitchOracle)); - emit AssetLTV0(asset2); - vm.expectEmit(address(killSwitchOracle)); - emit AssetFrozen(asset3); + emit BorrowDisabled(asset1); vm.prank(randomAddress); // Permissionless call killSwitchOracle.trigger(address(oracle1)); // Only update what has changed - reserves[0].ltv = 0; - reserves[1].ltv = 0; - reserves[2].frozen = true; + reserves[0].borrowingEnabled = false; for (uint256 i = 0; i < reserves.length; i++) { _assertReserve(reserves[i]); @@ -419,9 +382,6 @@ contract KillSwitchOracleTriggerTests is KillSwitchOracleTestBase { configuration.setFrozen(params.frozen); configuration.setPaused(params.paused); configuration.setBorrowingEnabled(params.borrowingEnabled); - configuration.setLtv(params.ltv); - configuration.setLiquidationThreshold(params.liquidationThreshold); - configuration.setLiquidationBonus(params.liquidationBonus); pool.__addReserve(params.asset, configuration); } @@ -429,13 +389,10 @@ contract KillSwitchOracleTriggerTests is KillSwitchOracleTestBase { function _assertReserve(ReserveConfigParams memory params) internal { DataTypes.ReserveConfigurationMap memory configuration = pool.getConfiguration(params.asset); - assertEq(configuration.getActive(), params.active); - assertEq(configuration.getFrozen(), params.frozen); - assertEq(configuration.getPaused(), params.paused); - assertEq(configuration.getBorrowingEnabled(), params.borrowingEnabled); - assertEq(configuration.getLtv(), params.ltv); - assertEq(configuration.getLiquidationThreshold(), params.liquidationThreshold); - assertEq(configuration.getLiquidationBonus(), params.liquidationBonus); + assertEq(configuration.getActive(), params.active); + assertEq(configuration.getFrozen(), params.frozen); + assertEq(configuration.getPaused(), params.paused); + assertEq(configuration.getBorrowingEnabled(), params.borrowingEnabled); } } diff --git a/test/KillSwitchOracleIntegration.t.sol b/test/KillSwitchOracleIntegration.t.sol index b343358..7bd28aa 100644 --- a/test/KillSwitchOracleIntegration.t.sol +++ b/test/KillSwitchOracleIntegration.t.sol @@ -8,7 +8,9 @@ import { IACLManager } from "lib/aave-v3-core/contracts/interfaces/IACL import { IPool } from "lib/aave-v3-core/contracts/interfaces/IPool.sol"; import { IPoolConfigurator } from "lib/aave-v3-core/contracts/interfaces/IPoolConfigurator.sol"; import { ReserveConfiguration } from "lib/aave-v3-core/contracts/protocol/libraries/configuration/ReserveConfiguration.sol"; +import { Errors } from "lib/aave-v3-core/contracts/protocol/libraries/helpers/Errors.sol"; import { DataTypes } from "lib/aave-v3-core/contracts/protocol/libraries/types/DataTypes.sol"; +import { IERC20 } from "lib/openzeppelin-contracts/contracts/token/ERC20/IERC20.sol"; import { KillSwitchOracle } from "src/KillSwitchOracle.sol"; @@ -17,8 +19,7 @@ contract KillSwitchOracleIntegrationTest is Test { using ReserveConfiguration for DataTypes.ReserveConfigurationMap; event Trigger(address indexed oracle, uint256 threshold, uint256 price); - event AssetLTV0(address indexed asset); - event AssetFrozen(address indexed asset); + event BorrowDisabled(address indexed asset); address constant POOL_ADDRESSES_PROVIDER = 0x02C3eA4e34C0cBd694D2adFa2c690EECbC1793eE; address constant POOL = 0xC13e21B648A5Ee794902342038FF3aDAB66BE987; @@ -39,6 +40,9 @@ contract KillSwitchOracleIntegrationTest is Test { address constant RETH = 0xae78736Cd615f374D3085123A210448E74Fc6393; address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant DAI_VAR_DEBT = 0xf705d2B7e92B3F38e6ae7afaDAA2fEE110fE5914; + address constant DAI_BORROWER_WALLET = 0xf8dE75c7B95edB6f1E639751318f117663021Cf0; + IPool pool = IPool(POOL); IPoolConfigurator poolConfigurator = IPoolConfigurator(POOL_CONFIGURATOR); IACLManager aclManager = IACLManager(ACL_MANAGER); @@ -76,212 +80,83 @@ contract KillSwitchOracleIntegrationTest is Test { vm.prank(randomUser); killSwitchOracle.trigger(WBTC_ORACLE); - _assertReserve({ - asset: DAI, - active: true, - frozen: false, - paused: false, - ltv: 0, - liquidationThreshold: 1, - liquidationBonus: 104_50 - }); - _assertReserve({ - asset: SDAI, - active: true, - frozen: false, - paused: false, - ltv: 74_00, - liquidationThreshold: 76_00, - liquidationBonus: 104_50 - }); - _assertReserve({ - asset: USDC, - active: true, - frozen: false, - paused: false, - ltv: 0, - liquidationThreshold: 0, - liquidationBonus: 0 - }); - _assertReserve({ - asset: WETH, - active: true, - frozen: false, - paused: false, - ltv: 80_00, - liquidationThreshold: 82_50, - liquidationBonus: 105_00 - }); - _assertReserve({ - asset: WSTETH, - active: true, - frozen: false, - paused: false, - ltv: 68_50, - liquidationThreshold: 79_50, - liquidationBonus: 107_00 - }); - _assertReserve({ - asset: WBTC, - active: true, - frozen: false, - paused: false, - ltv: 70_00, - liquidationThreshold: 75_00, - liquidationBonus: 107_00 - }); - _assertReserve({ - asset: GNO, - active: true, - frozen: true, - paused: false, - ltv: 0, - liquidationThreshold: 25_00, - liquidationBonus: 110_00 - }); - _assertReserve({ - asset: RETH, - active: true, - frozen: false, - paused: false, - ltv: 68_50, - liquidationThreshold: 79_50, - liquidationBonus: 107_00 - }); - _assertReserve({ - asset: USDT, - active: true, - frozen: false, - paused: false, - ltv: 0, - liquidationThreshold: 0, - liquidationBonus: 0 - }); + assertEq(_getBorrowEnabled(DAI), true); + assertEq(_getBorrowEnabled(SDAI), false); + assertEq(_getBorrowEnabled(USDC), true); + assertEq(_getBorrowEnabled(WETH), true); + assertEq(_getBorrowEnabled(WSTETH), true); + assertEq(_getBorrowEnabled(WBTC), true); + assertEq(_getBorrowEnabled(GNO), false); + assertEq(_getBorrowEnabled(RETH), true); + assertEq(_getBorrowEnabled(USDT), true); vm.expectEmit(address(killSwitchOracle)); emit Trigger(address(STETH_ORACLE), 0.9999e18, 0.999599998787617000e18); vm.expectEmit(address(killSwitchOracle)); - emit AssetFrozen(DAI); - vm.expectEmit(address(killSwitchOracle)); - emit AssetLTV0(SDAI); + emit BorrowDisabled(DAI); vm.expectEmit(address(killSwitchOracle)); - emit AssetFrozen(USDC); + emit BorrowDisabled(USDC); vm.expectEmit(address(killSwitchOracle)); - emit AssetLTV0(WETH); + emit BorrowDisabled(WETH); vm.expectEmit(address(killSwitchOracle)); - emit AssetLTV0(WSTETH); + emit BorrowDisabled(WSTETH); vm.expectEmit(address(killSwitchOracle)); - emit AssetLTV0(WBTC); + emit BorrowDisabled(WBTC); vm.expectEmit(address(killSwitchOracle)); - emit AssetLTV0(RETH); + emit BorrowDisabled(RETH); vm.expectEmit(address(killSwitchOracle)); - emit AssetFrozen(USDT); + emit BorrowDisabled(USDT); vm.prank(randomUser); killSwitchOracle.trigger(STETH_ORACLE); - _assertReserve({ - asset: DAI, - active: true, - frozen: true, - paused: false, - ltv: 0, - liquidationThreshold: 1, - liquidationBonus: 104_50 - }); - _assertReserve({ - asset: SDAI, - active: true, - frozen: false, - paused: false, - ltv: 0, - liquidationThreshold: 76_00, - liquidationBonus: 104_50 - }); - _assertReserve({ - asset: USDC, - active: true, - frozen: true, - paused: false, - ltv: 0, - liquidationThreshold: 0, - liquidationBonus: 0 - }); - _assertReserve({ - asset: WETH, - active: true, - frozen: false, - paused: false, - ltv: 0, - liquidationThreshold: 82_50, - liquidationBonus: 105_00 - }); - _assertReserve({ - asset: WSTETH, - active: true, - frozen: false, - paused: false, - ltv: 0, - liquidationThreshold: 79_50, - liquidationBonus: 107_00 - }); - _assertReserve({ - asset: WBTC, - active: true, - frozen: false, - paused: false, - ltv: 0, - liquidationThreshold: 75_00, - liquidationBonus: 107_00 - }); - _assertReserve({ - asset: GNO, - active: true, - frozen: true, - paused: false, - ltv: 0, - liquidationThreshold: 25_00, - liquidationBonus: 110_00 - }); - _assertReserve({ - asset: RETH, - active: true, - frozen: false, - paused: false, - ltv: 0, - liquidationThreshold: 79_50, - liquidationBonus: 107_00 - }); - _assertReserve({ - asset: USDT, - active: true, - frozen: true, - paused: false, - ltv: 0, - liquidationThreshold: 0, - liquidationBonus: 0 - }); - } - - function _assertReserve( - address asset, - bool active, - bool frozen, - bool paused, - uint256 ltv, - uint256 liquidationThreshold, - uint256 liquidationBonus - ) internal { - DataTypes.ReserveConfigurationMap memory configuration = pool.getConfiguration(asset); + assertEq(_getBorrowEnabled(DAI), false); + assertEq(_getBorrowEnabled(SDAI), false); + assertEq(_getBorrowEnabled(USDC), false); + assertEq(_getBorrowEnabled(WETH), false); + assertEq(_getBorrowEnabled(WSTETH), false); + assertEq(_getBorrowEnabled(WBTC), false); + assertEq(_getBorrowEnabled(GNO), false); + assertEq(_getBorrowEnabled(RETH), false); + assertEq(_getBorrowEnabled(USDT), false); + + // Test the functionality of the pool + uint256 userBalance = 40_187_695.578838876771725671e18; + deal(DAI, DAI_BORROWER_WALLET, 1e18); + assertEq(IERC20(DAI_VAR_DEBT).balanceOf(DAI_BORROWER_WALLET), userBalance); - assertEq(configuration.getActive(), active); - assertEq(configuration.getFrozen(), frozen); - assertEq(configuration.getPaused(), paused); - assertEq(configuration.getLtv(), ltv); - assertEq(configuration.getLiquidationThreshold(), liquidationThreshold); - assertEq(configuration.getLiquidationBonus(), liquidationBonus); + vm.startPrank(DAI_BORROWER_WALLET); + + // Make sure we can repay + IERC20(DAI).approve(address(pool), 1e18); + pool.repay(DAI, 1e18, 2, DAI_BORROWER_WALLET); + + // Borrow should revert on all assets + vm.expectRevert(bytes(Errors.BORROWING_NOT_ENABLED)); + pool.borrow(DAI, 1, 2, 0, DAI_BORROWER_WALLET); + vm.expectRevert(bytes(Errors.BORROWING_NOT_ENABLED)); + pool.borrow(USDC, 1, 2, 0, DAI_BORROWER_WALLET); + vm.expectRevert(bytes(Errors.BORROWING_NOT_ENABLED)); + pool.borrow(WETH, 1, 2, 0, DAI_BORROWER_WALLET); + vm.expectRevert(bytes(Errors.BORROWING_NOT_ENABLED)); + pool.borrow(WSTETH, 1, 2, 0, DAI_BORROWER_WALLET); + vm.expectRevert(bytes(Errors.BORROWING_NOT_ENABLED)); + pool.borrow(WBTC, 1, 2, 0, DAI_BORROWER_WALLET); + vm.expectRevert(bytes(Errors.BORROWING_NOT_ENABLED)); + pool.borrow(RETH, 1, 2, 0, DAI_BORROWER_WALLET); + vm.expectRevert(bytes(Errors.BORROWING_NOT_ENABLED)); + pool.borrow(USDT, 1, 2, 0, DAI_BORROWER_WALLET); + vm.expectRevert(bytes(Errors.RESERVE_FROZEN)); + pool.borrow(GNO, 1, 2, 0, DAI_BORROWER_WALLET); + vm.expectRevert(bytes(Errors.BORROWING_NOT_ENABLED)); + pool.borrow(SDAI, 1, 2, 0, DAI_BORROWER_WALLET); + + vm.stopPrank(); + + assertEq(IERC20(DAI_VAR_DEBT).balanceOf(DAI_BORROWER_WALLET), userBalance - 1e18); } - // TODO add some more specific checks to make sure existing users can top up collateral, repay loans and withdraw collateral. - // Also demonstate that users can't be liquidate or anything weird. + function _getBorrowEnabled(address asset) internal view returns (bool) { + return pool.getConfiguration(asset).getBorrowingEnabled(); + } } diff --git a/test/mocks/MockPoolConfigurator.sol b/test/mocks/MockPoolConfigurator.sol index 14f9926..e3ba9d4 100644 --- a/test/mocks/MockPoolConfigurator.sol +++ b/test/mocks/MockPoolConfigurator.sol @@ -15,22 +15,9 @@ contract MockPoolConfigurator { pool = _pool; } - function configureReserveAsCollateral( - address asset, - uint256 ltv, - uint256 liquidationThreshold, - uint256 liquidationBonus - ) external { + function setReserveBorrowing(address asset, bool borrowEnabled) external { DataTypes.ReserveConfigurationMap memory configuration = pool.getConfiguration(asset); - configuration.setLtv(ltv); - configuration.setLiquidationThreshold(liquidationThreshold); - configuration.setLiquidationBonus(liquidationBonus); - pool.setConfiguration(asset, configuration); - } - - function setReserveFreeze(address asset, bool freeze) external { - DataTypes.ReserveConfigurationMap memory configuration = pool.getConfiguration(asset); - configuration.setFrozen(freeze); + configuration.setBorrowingEnabled(borrowEnabled); pool.setConfiguration(asset, configuration); }