diff --git a/contracts/Comptroller.sol b/contracts/Comptroller.sol index 03d228ba5..7675955e0 100644 --- a/contracts/Comptroller.sol +++ b/contracts/Comptroller.sol @@ -340,7 +340,7 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE } /* Otherwise, perform a hypothetical liquidity check to guard against shortfall */ - (Error err, , uint256 shortfall) = getHypotheticalAccountLiquidityInternal( + (Error err, , uint256 shortfall, , ) = getHypotheticalAccountLiquidityInternal( redeemer, CToken(cToken), redeemTokens, @@ -424,7 +424,7 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE require(nextTotalBorrows < borrowCap, "market borrow cap reached"); } - (Error err, , uint256 shortfall) = getHypotheticalAccountLiquidityInternal( + (Error err, , uint256 shortfall, , ) = getHypotheticalAccountLiquidityInternal( borrower, CToken(cToken), 0, @@ -769,7 +769,7 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE uint256 ) { - (Error err, uint256 liquidity, uint256 shortfall) = getHypotheticalAccountLiquidityInternal( + (Error err, uint256 liquidity, uint256 shortfall, , ) = getHypotheticalAccountLiquidityInternal( account, CToken(0), 0, @@ -779,6 +779,27 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE return (uint256(err), liquidity, shortfall); } + /** + * @notice Get user current health factor + * @dev The higher the helath factor is, the safer the state of the user's funds are against a liquidation scenario. + * If the health factor reaches 1e18, the liquidation of your deposits will be triggered. + * @return The user health factor, scaled by 1e18 + */ + function getUserHealthFactor(address account) public view returns (uint256) { + (Error err, , , uint256 totalCollateral, uint256 totalBorrow) = getHypotheticalAccountLiquidityInternal( + account, + CToken(0), + 0, + 0 + ); + require(err == Error.NO_ERROR, "failed to get account liquidity"); + + if (totalBorrow == 0) { + return uint256(-1); + } + return div_(totalCollateral, Exp({mantissa: totalBorrow})); + } + /** * @notice Determine the current account liquidity wrt collateral requirements * @return (possible error code, @@ -794,7 +815,13 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE uint256 ) { - return getHypotheticalAccountLiquidityInternal(account, CToken(0), 0, 0); + (Error err, uint256 liquidity, uint256 shortfall, , ) = getHypotheticalAccountLiquidityInternal( + account, + CToken(0), + 0, + 0 + ); + return (err, liquidity, shortfall); } /** @@ -821,7 +848,7 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE uint256 ) { - (Error err, uint256 liquidity, uint256 shortfall) = getHypotheticalAccountLiquidityInternal( + (Error err, uint256 liquidity, uint256 shortfall, , ) = getHypotheticalAccountLiquidityInternal( account, CToken(cTokenModify), redeemTokens, @@ -839,8 +866,10 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE * @dev Note that we calculate the exchangeRateStored for each collateral cToken using stored data, * without calculating accumulated interest. * @return (possible error code, - hypothetical account liquidity in excess of collateral requirements, - * hypothetical account shortfall below collateral requirements) + * hypothetical account liquidity in excess of collateral requirements, + * hypothetical account shortfall below collateral requirements, + * hypothetical account total collateral value, + * hypothetical account total borrow value) */ function getHypotheticalAccountLiquidityInternal( address account, @@ -853,6 +882,8 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE returns ( Error, uint256, + uint256, + uint256, uint256 ) { @@ -870,7 +901,7 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE ); if (oErr != 0) { // semi-opaque error code, we assume NO_ERROR == 0 is invariant between upgrades - return (Error.SNAPSHOT_ERROR, 0, 0); + return (Error.SNAPSHOT_ERROR, 0, 0, 0, 0); } // Unlike compound protocol, getUnderlyingPrice is relatively expensive because we use ChainLink as our primary price feed. @@ -885,7 +916,7 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE // Get the normalized price of the asset vars.oraclePriceMantissa = oracle.getUnderlyingPrice(asset); if (vars.oraclePriceMantissa == 0) { - return (Error.PRICE_ERROR, 0, 0); + return (Error.PRICE_ERROR, 0, 0, 0, 0); } vars.oraclePrice = Exp({mantissa: vars.oraclePriceMantissa}); @@ -924,9 +955,21 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE // These are safe, as the underflow condition is checked first if (vars.sumCollateral > vars.sumBorrowPlusEffects) { - return (Error.NO_ERROR, vars.sumCollateral - vars.sumBorrowPlusEffects, 0); + return ( + Error.NO_ERROR, + vars.sumCollateral - vars.sumBorrowPlusEffects, + 0, + vars.sumCollateral, + vars.sumBorrowPlusEffects + ); } else { - return (Error.NO_ERROR, 0, vars.sumBorrowPlusEffects - vars.sumCollateral); + return ( + Error.NO_ERROR, + 0, + vars.sumBorrowPlusEffects - vars.sumCollateral, + vars.sumCollateral, + vars.sumBorrowPlusEffects + ); } } diff --git a/tests/Comptroller/accountLiquidityTest.js b/tests/Comptroller/accountLiquidityTest.js index a9e7305ac..b3d60b6da 100644 --- a/tests/Comptroller/accountLiquidityTest.js +++ b/tests/Comptroller/accountLiquidityTest.js @@ -4,6 +4,10 @@ const { enterMarkets, quickMint } = require('../Utils/Compound'); +const { + etherMantissa, + UInt256Max +} = require('../Utils/Ethereum'); describe('Comptroller', () => { let root, accounts; @@ -20,6 +24,8 @@ describe('Comptroller', () => { await quickMint(cToken, user, amount); let result = await call(cToken.comptroller, 'getAccountLiquidity', [user]); expect(result).toHaveTrollError('PRICE_ERROR'); + + await expect(call(cToken.comptroller, 'getUserHealthFactor', [user])).rejects.toRevert('revert failed to get account liquidity'); }); it("allows a borrow up to collateralFactor, but not more", async () => { @@ -71,6 +77,9 @@ describe('Comptroller', () => { expect(liquidity).toEqualNumber(collateral); expect(shortfall).toEqualNumber(0); + const healthFactor = await call(cToken3.comptroller, 'getUserHealthFactor', [user]); + expect(healthFactor).toEqualNumber(UInt256Max()); + ({1: liquidity, 2: shortfall} = await call(cToken3.comptroller, 'getHypotheticalAccountLiquidity', [user, cToken3._address, Math.floor(c2), 0])); expect(liquidity).toEqualNumber(collateral); expect(shortfall).toEqualNumber(0); @@ -122,4 +131,29 @@ describe('Comptroller', () => { expect(shortfall).toEqualNumber(0); }); }); + + describe("getUserHealthFactor", () => { + it('gets the user health factor', async () => { + const amount1 = 1e6, amount2 = 1e3, amount3 = 1e3, user = accounts[1]; + const cf1 = 0.5, cf2 = 0.5, up1 = 1, up2 = 100; + const c1 = amount1 * cf1 * up1, c2 = amount2 * cf2 * up2 + const collateral = Math.floor(c1 + c2); + const borrow = amount3 * up1; + const cToken1 = await makeCToken({supportMarket: true, collateralFactor: cf1, underlyingPrice: up1}); + const cToken2 = await makeCToken({supportMarket: true, comptroller: cToken1.comptroller, collateralFactor: cf2, underlyingPrice: up2}); + + await enterMarkets([cToken1, cToken2], user); + await quickMint(cToken1, user, amount1); + await quickMint(cToken2, user, amount2); + await send(cToken1, 'harnessBorrowFresh', [user, amount3]); + + ({0: error, 1: liquidity, 2: shortfall} = await call(cToken2.comptroller, 'getAccountLiquidity', [user])); + expect(error).toEqualNumber(0); + expect(liquidity).toEqualNumber(collateral - borrow); + expect(shortfall).toEqualNumber(0); + + const healthFactor = await call(cToken2.comptroller, 'getUserHealthFactor', [user]); + expect(healthFactor).toEqualNumber(etherMantissa(collateral / borrow)); + }); + }); });