Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 54 additions & 11 deletions contracts/Comptroller.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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);
}

/**
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -853,6 +882,8 @@ contract Comptroller is ComptrollerV7Storage, ComptrollerInterface, ComptrollerE
returns (
Error,
uint256,
uint256,
uint256,
uint256
)
{
Expand All @@ -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.
Expand All @@ -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});

Expand Down Expand Up @@ -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
);
}
}

Expand Down
34 changes: 34 additions & 0 deletions tests/Comptroller/accountLiquidityTest.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ const {
enterMarkets,
quickMint
} = require('../Utils/Compound');
const {
etherMantissa,
UInt256Max
} = require('../Utils/Ethereum');

describe('Comptroller', () => {
let root, accounts;
Expand All @@ -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 () => {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
});
});
});