diff --git a/TODO b/TODO index 8025eaeb..1e78d77c 100644 --- a/TODO +++ b/TODO @@ -1,3 +1,11 @@ +Overrides: + * liquidations: will incorrectly calculate liquidatable amount + * verify adding fields to the end of LiquidityStatus doesn't break any ABIs + * view can't correctly query collateral/liability values of individual assets + ? fix this in riskManager.computeAssetLiquidities + * make sure user isn't entered into any other markets (with 0 balance) + * attacker could dust their account and force-disable the override + Lending logic: g when a token has < 18 decimal places, and a user withdraws their full EToken balance, 0 out the remaining dust so user gets a storage refund diff --git a/contracts/Events.sol b/contracts/Events.sol index dc295ede..cc1a6155 100644 --- a/contracts/Events.sol +++ b/contracts/Events.sol @@ -57,6 +57,7 @@ abstract contract Events { event GovSetReserveFee(address indexed underlying, uint32 newReserveFee); event GovConvertReserves(address indexed underlying, address indexed recipient, uint amount); event GovSetChainlinkPriceFeed(address indexed underlying, address chainlinkAggregator); + event GovSetOverride(address indexed liability, address indexed collateral, Storage.OverrideConfig newOverride); event RequestSwap(address indexed accountIn, address indexed accountOut, address indexed underlyingIn, address underlyingOut, uint amount, uint swapType); event RequestSwapHub(address indexed accountIn, address indexed accountOut, address indexed underlyingIn, address underlyingOut, uint amountIn, uint amountOut, uint mode, address swapHandler); diff --git a/contracts/IRiskManager.sol b/contracts/IRiskManager.sol index b13d33f1..6ab4afe6 100644 --- a/contracts/IRiskManager.sol +++ b/contracts/IRiskManager.sol @@ -19,6 +19,8 @@ interface IRiskManager { uint liabilityValue; uint numBorrows; bool borrowIsolated; + uint numCollaterals; + bool overrideEnabled; } struct AssetLiquidity { diff --git a/contracts/Storage.sol b/contracts/Storage.sol index 7bd1a290..e7de55af 100644 --- a/contracts/Storage.sol +++ b/contracts/Storage.sol @@ -93,4 +93,11 @@ abstract contract Storage is Constants { mapping(address => address) internal pTokenLookup; // PToken => underlying mapping(address => address) internal reversePTokenLookup; // underlying => PToken mapping(address => address) internal chainlinkPriceFeedLookup; // underlying => chainlinkAggregator + + struct OverrideConfig { + bool enabled; + uint32 collateralFactor; + } + + mapping(address => mapping(address => OverrideConfig)) internal overrideLookup; // liability => collateral => OverrideConfig } diff --git a/contracts/modules/Governance.sol b/contracts/modules/Governance.sol index c5b7f539..e9e8d9cf 100644 --- a/contracts/modules/Governance.sol +++ b/contracts/modules/Governance.sol @@ -117,6 +117,14 @@ contract Governance is BaseLogic { emit GovSetChainlinkPriceFeed(underlying, chainlinkAggregator); } + function setOverride(address liability, address collateral, OverrideConfig calldata newOverride) external nonReentrant governorOnly { + require(underlyingLookup[liability].eTokenAddress != address(0), "e/gov/liability-not-activated"); + require(underlyingLookup[collateral].eTokenAddress != address(0), "e/gov/collateral-not-activated"); + + overrideLookup[liability][collateral] = newOverride; + + emit GovSetOverride(liability, collateral, newOverride); + } // getters diff --git a/contracts/modules/RiskManager.sol b/contracts/modules/RiskManager.sol index 7b5a1dd9..683e8f24 100644 --- a/contracts/modules/RiskManager.sol +++ b/contracts/modules/RiskManager.sol @@ -287,12 +287,23 @@ contract RiskManager is IRiskManager, BaseLogic { // Liquidity + struct OverrideCache { + address liability; + uint liabilityValue; + + address collateral; + uint collateralValue; + } + function computeLiquidityRaw(address account, address[] memory underlyings) private view returns (LiquidityStatus memory status) { status.collateralValue = 0; status.liabilityValue = 0; status.numBorrows = 0; status.borrowIsolated = false; + status.numCollaterals = 0; + status.overrideEnabled = false; + OverrideCache memory overrideCache; AssetConfig memory config; AssetStorage storage assetStorage; AssetCache memory assetCache; @@ -311,11 +322,16 @@ contract RiskManager is IRiskManager, BaseLogic { (uint price,) = getPriceInternal(assetCache, config); status.numBorrows++; + overrideCache.liability = underlying; + if (config.borrowIsolated) status.borrowIsolated = true; uint assetLiability = getCurrentOwed(assetStorage, assetCache, account); if (balance != 0) { // self-collateralisation + status.numCollaterals++; + overrideCache.collateral = underlying; + uint balanceInUnderlying = balanceToUnderlyingAmount(assetCache, balance); uint selfAmount = assetLiability; @@ -337,19 +353,32 @@ contract RiskManager is IRiskManager, BaseLogic { status.borrowIsolated = true; // self-collateralised loans are always isolated } - assetLiability = assetLiability * price / 1e18; + assetLiability = overrideCache.liabilityValue = assetLiability * price / 1e18; assetLiability = config.borrowFactor != 0 ? assetLiability * CONFIG_FACTOR_SCALE / config.borrowFactor : MAX_SANE_DEBT_AMOUNT; status.liabilityValue += assetLiability; } else if (balance != 0 && config.collateralFactor != 0) { initAssetCache(underlying, assetStorage, assetCache); (uint price,) = getPriceInternal(assetCache, config); + status.numCollaterals++; + overrideCache.collateral = underlying; + uint balanceInUnderlying = balanceToUnderlyingAmount(assetCache, balance); - uint assetCollateral = balanceInUnderlying * price / 1e18; + uint assetCollateral = overrideCache.collateralValue = balanceInUnderlying * price / 1e18; assetCollateral = assetCollateral * config.collateralFactor / CONFIG_FACTOR_SCALE; status.collateralValue += assetCollateral; } } + + if (status.numBorrows == 1 && status.numCollaterals == 1 && overrideCache.liability != overrideCache.collateral) { + OverrideConfig memory overrideConfig = overrideLookup[overrideCache.liability][overrideCache.collateral]; + + if (overrideConfig.enabled) { + status.overrideEnabled = true; + status.collateralValue = overrideCache.collateralValue * overrideConfig.collateralFactor / CONFIG_FACTOR_SCALE; + status.liabilityValue = overrideCache.liabilityValue; + } + } } function computeLiquidity(address account) public view override returns (LiquidityStatus memory) { diff --git a/test/override.js b/test/override.js new file mode 100644 index 00000000..ded7d535 --- /dev/null +++ b/test/override.js @@ -0,0 +1,69 @@ +const et = require('./lib/eTestLib'); +const scenarios = require('./lib/scenarios'); + + +et.testSet({ + desc: "overrides", + + preActions: ctx => [ + ...scenarios.basicLiquidity()(ctx), + { send: 'tokens.TST3.approve', args: [ctx.contracts.euler.address, et.MaxUint256,], }, + { send: 'tokens.TST3.mint', args: [ctx.wallet.address, et.eth(100)], }, + { send: 'eTokens.eTST3.deposit', args: [0, et.eth(10)], }, + + { action: 'updateUniswapPrice', pair: 'TST/WETH', price: '2', }, + { action: 'updateUniswapPrice', pair: 'TST2/WETH', price: '0.5', }, + { action: 'updateUniswapPrice', pair: 'TST3/WETH', price: '0.25', }, + + { action: 'setAssetConfig', tok: 'TST3', config: { borrowIsolated: false, borrowFactor: .5, }, }, + ], +}) + + + +.test({ + desc: "override basic", + actions: ctx => [ + { from: ctx.wallet2, send: 'dTokens.dTST.borrow', args: [0, et.eth(.1)], }, + + // Account starts off normal, with single collateral and single borrow + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 0.5, .001); // 0.1 * 2 / 0.4 + et.equals(r.collateralValue, 3.75, .001); // 10 * 0.5 * 0.75 + et.expect(r.overrideEnabled).to.equal(false); + }, }, + + // Override is added for this liability/collateral pair + + { send: 'governance.setOverride', args: [ + ctx.contracts.tokens.TST.address, + ctx.contracts.tokens.TST2.address, + { + enabled: true, + collateralFactor: Math.floor(0.97 * 4e9), + }, + ], }, + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 0.2, .001); // 0.1 * 2 + et.equals(r.collateralValue, 4.85, .001); // 10 * 0.5 * 0.97 + et.expect(r.overrideEnabled).to.equal(true); + }, }, + + { from: ctx.wallet2, send: 'dTokens.dTST3.borrow', args: [0, et.eth(.1)], }, + + // Additional borrow on account disables override + + { call: 'exec.liquidity', args: [ctx.wallet2.address], onResult: r => { + et.equals(r.liabilityValue, 0.55, .001); // (0.1 * 2 / 0.4) + (0.1 * 0.25 / 0.5) + et.equals(r.collateralValue, 3.75, .001); // 10 * 0.5 * 0.75 + et.expect(r.overrideEnabled).to.equal(false); + }, }, + ], +}) + + + + +.run();