Skip to content

Commit

Permalink
feat: first draft
Browse files Browse the repository at this point in the history
  • Loading branch information
0xmikko committed Apr 3, 2023
1 parent b1aeef7 commit 59b2779
Show file tree
Hide file tree
Showing 9 changed files with 356 additions and 313 deletions.
209 changes: 154 additions & 55 deletions contracts/credit/CreditFacade.sol

Large diffs are not rendered by default.

179 changes: 78 additions & 101 deletions contracts/credit/CreditManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
TokenLT[] memory tokens;

if (supportsQuotas) {
tokens = getLimitedTokens(creditAccount);
tokens = getQuotedTokens(creditAccount);

// TODO: Check that it never breaks
uint256 quotaInterest = cumulativeQuotaInterest[creditAccount] - 1;
Expand Down Expand Up @@ -421,11 +421,14 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {

uint256 newCumulativeIndex;
if (increase) {
// Checks that there are no forbidden tokens, as borrowing
// is prohibited when forbidden tokens are enabled on the account
_checkForbiddenTokens(creditAccount);

newBorrowedAmount = borrowedAmount + amount;

// Computes the new cumulative index to keep the interest
// unchanged with different principal

newCumulativeIndex =
_calcNewCumulativeIndex(borrowedAmount, amount, cumulativeIndexNow_RAY, cumulativeIndexAtOpen_RAY, true);

Expand Down Expand Up @@ -504,6 +507,27 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
}
}

/// @dev Checks that there are no intersections between the user's enabled tokens
/// and the set of forbidden tokens
/// @notice The main purpose of forbidding tokens is to prevent exposing
/// pool funds to dangerous or exploited collateral, without immediately
/// liquidating accounts that hold the forbidden token
/// There are two ways pool funds can be exposed:
/// - The CA owner tries to swap borrowed funds to the forbidden asset:
/// this will be blocked by checkAndEnableToken, which is invoked for tokenOut
/// after every operation;
/// - The CA owner with an already enabled forbidden token transfers it
/// to the account - they can't use addCollateral / enableToken due to checkAndEnableToken,
/// but can transfer the token directly when it is enabled and it will be counted in the collateral -
/// an borrows against it. This check is used to prevent this.
/// If the owner has a forbidden token and want to take more debt, they must first
/// dispose of the token and disable it.
function _checkForbiddenTokens(address creditAccount) internal view {
if (enabledTokensMap[creditAccount] & forbiddenTokenMask > 0) {
revert ForbiddenTokensException();
}
}

function _computeQuotasAmountDebtDecrease(address creditAccount, uint256 _amountRepaid, uint256 _amountProfit)
internal
returns (uint256 amountRepaid, uint256 amountProfit)
Expand All @@ -514,7 +538,7 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
uint16 feeInterest = slot1.feeInterest;
uint256 quotaInterestAccrued = cumulativeQuotaInterest[creditAccount];

TokenLT[] memory tokens = getLimitedTokens(creditAccount);
TokenLT[] memory tokens = getQuotedTokens(creditAccount);
if (tokens.length > 0) {
quotaInterestAccrued += poolQuotaKeeper().accrueQuotaInterest(creditAccount, tokens); // F: [CMQ-4,5]
}
Expand Down Expand Up @@ -751,88 +775,72 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
/// @param minHealthFactor Minimal health factor of the account, in PERCENTAGE format
function fullCollateralCheck(address creditAccount, uint256[] memory collateralHints, uint16 minHealthFactor)
external
adaptersOrCreditFacadeOnly
creditFacadeOnly
nonReentrant
returns (uint256 enabledTokenMask)
{
if (minHealthFactor < PERCENTAGE_FACTOR) {
revert CustomHealthFactorTooLowException();
}

_fullCollateralCheck(creditAccount, collateralHints, minHealthFactor);
}

/// @dev IMPLEMENTATION: fullCollateralCheck
function _fullCollateralCheck(address creditAccount, uint256[] memory collateralHints, uint16 minHealthFactor)
internal
virtual
{
IPriceOracleV2 _priceOracle = slot1.priceOracle;

uint256 enabledTokenMask = enabledTokensMap[creditAccount];
enabledTokenMask = enabledTokensMap[creditAccount];
uint256 checkedTokenMask = enabledTokenMask;
uint256 borrowAmountPlusInterestRateUSD;

uint256 twvUSD;

{
uint256 quotaInterest;
if (supportsQuotas) {
TokenLT[] memory tokens = getLimitedTokens(creditAccount);

if (tokens.length > 0) {
/// If credit account has any connected token - then check that
(twvUSD, quotaInterest) = poolQuotaKeeper().computeQuotedCollateralUSD(
address(this), creditAccount, address(_priceOracle), tokens
); // F: [CMQ-8]
uint256 quotaInterest;
if (supportsQuotas) {
TokenLT[] memory tokens = getQuotedTokens(creditAccount);

checkedTokenMask = checkedTokenMask & (~limitedTokenMask);
}
if (tokens.length > 0) {
/// If credit account has any connected token - then check that
(twvUSD, quotaInterest) = poolQuotaKeeper().computeQuotedCollateralUSD(
address(this), creditAccount, address(_priceOracle), tokens
); // F: [CMQ-8]

quotaInterest += cumulativeQuotaInterest[creditAccount]; // F: [CMQ-8]
checkedTokenMask = checkedTokenMask & (~limitedTokenMask);
}

// The total weighted value of a Credit Account has to be compared
// with the entire debt sum, including interest and fees
(,, uint256 borrowedAmountWithInterestAndFees) =
_calcCreditAccountAccruedInterest(creditAccount, quotaInterest);
quotaInterest += cumulativeQuotaInterest[creditAccount]; // F: [CMQ-8]
}

borrowAmountPlusInterestRateUSD = _priceOracle.convertToUSD(
borrowedAmountWithInterestAndFees * minHealthFactor, // F: [CM-42]
underlying
);
// The total weighted value of a Credit Account has to be compared
// with the entire debt sum, including interest and fees
(,, uint256 borrowedAmountWithInterestAndFees) = _calcCreditAccountAccruedInterest(creditAccount, quotaInterest);

borrowAmountPlusInterestRateUSD = _priceOracle.convertToUSD(
borrowedAmountWithInterestAndFees * minHealthFactor, // F: [CM-42]
underlying
);

// If quoted tokens fully cover the debt, we can stop here
// after performing some additional cleanup
if (twvUSD >= borrowAmountPlusInterestRateUSD) {
// F: [CMQ-9]
_afterFullCheck(creditAccount, enabledTokenMask, false);
// If quoted tokens fully cover the debt, we can stop here
// after performing some additional cleanup
if (twvUSD < borrowAmountPlusInterestRateUSD) {
uint256 tokensToDisable = _checkNonLimitedTokens(
creditAccount, checkedTokenMask, twvUSD, borrowAmountPlusInterestRateUSD, collateralHints, _priceOracle
);

return;
if (tokensToDisable != 0) {
enabledTokenMask = enabledTokenMask & (~tokensToDisable);
enabledTokensMap[creditAccount] = enabledTokenMask;
}
}

_checkNonLimitedTokens(
creditAccount,
enabledTokenMask,
checkedTokenMask,
twvUSD,
borrowAmountPlusInterestRateUSD,
collateralHints,
_priceOracle
);
_checkEnabledTokenLength(enabledTokenMask);
}

function _checkNonLimitedTokens(
address creditAccount,
uint256 enabledTokenMask,
uint256 checkedTokenMask,
uint256 twvUSD,
uint256 borrowAmountPlusInterestRateUSD,
uint256[] memory collateralHints,
IPriceOracleV2 _priceOracle
) internal {
) internal returns (uint256 tokensToDisable) {
uint256 tokenMask;
bool atLeastOneTokenWasDisabled;

uint256 len = collateralHints.length;
uint256 i;
Expand All @@ -856,18 +864,13 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
// Once the TWV computed thus far exceeds the debt, the check is considered
// successful, and the function returns without evaluating any further collateral
if (twvUSD >= borrowAmountPlusInterestRateUSD) {
// The _afterFullCheck hook does some cleanup, such as disabling
// zero-balance tokens
_afterFullCheck(creditAccount, enabledTokenMask, atLeastOneTokenWasDisabled);

return; // F:[CM-40]
return tokensToDisable;
}
// Zero-balance tokens are disabled; this is done by flipping the
// bit in enabledTokenMask, which is then written into storage at the
// very end, to avoid redundant storage writes
} else {
enabledTokenMask &= ~tokenMask; // F:[CM-39]
atLeastOneTokenWasDisabled = true; // F:[CM-39]
tokensToDisable |= tokenMask; // F:[CM-39]
}
}

Expand All @@ -879,40 +882,22 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
revert NotEnoughCollateralException();
}

function _afterFullCheck(address creditAccount, uint256 enabledTokenMask, bool atLeastOneTokenWasDisabled)
internal
{
uint256 totalTokensEnabled = _calcEnabledTokens(enabledTokenMask);
if (totalTokensEnabled > maxAllowedEnabledTokenLength) {
revert TooManyEnabledTokensException();
} else {
// Saves enabledTokensMask if at least one token was disabled
if (atLeastOneTokenWasDisabled) {
enabledTokensMap[creditAccount] = enabledTokenMask; // F:[CM-39]
}
}
}

/// @dev Returns the array of quoted tokens that are enabled on the account
function getLimitedTokens(address creditAccount) public view returns (TokenLT[] memory tokens) {
uint256 limitMask = enabledTokensMap[creditAccount] & limitedTokenMask;
function getQuotedTokens(address creditAccount) public view returns (TokenLT[] memory tokens) {
uint256 quotedMask = enabledTokensMap[creditAccount] & limitedTokenMask;

if (limitMask > 0) {
if (quotedMask > 0) {
tokens = new TokenLT[](maxAllowedEnabledTokenLength + 1);

uint256 tokenMask = 2;

uint256 j;

unchecked {
while (tokenMask <= limitMask) {
if (limitMask & tokenMask != 0) {
for (uint256 tokenMask = 2; tokenMask <= quotedMask; tokenMask <<= 1) {
if (quotedMask & tokenMask != 0) {
(address token, uint16 lt) = collateralTokensByMask(tokenMask);
tokens[j] = TokenLT({token: token, lt: lt});
++j;
}

tokenMask = tokenMask << 1;
}
}
}
Expand All @@ -921,15 +906,13 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
/// @dev Checks that the number of enabled tokens on a Credit Account
/// does not violate the maximal enabled token limit
/// @param creditAccount Account to check enabled tokens for
function checkEnabledTokensLength(address creditAccount)
external
view
override
adaptersOrCreditFacadeOnly // F:[CM-3]
{
function checkEnabledTokensLength(address creditAccount) external view override {
uint256 enabledTokenMask = enabledTokensMap[creditAccount];
uint256 totalTokensEnabled = _calcEnabledTokens(enabledTokenMask);
_checkEnabledTokenLength(enabledTokenMask);
}

function _checkEnabledTokenLength(uint256 enabledTokenMask) internal view {
uint256 totalTokensEnabled = _calcEnabledTokens(enabledTokenMask);
if (totalTokensEnabled > maxAllowedEnabledTokenLength) {
revert TooManyEnabledTokensException();
}
Expand All @@ -948,7 +931,7 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
unchecked {
while (enabledTokenMask > 0) {
totalTokensEnabled += enabledTokenMask & 1;
enabledTokenMask = enabledTokenMask >> 1;
enabledTokenMask >>= 1;
}
}
}
Expand Down Expand Up @@ -1012,10 +995,10 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
tokensToEnable &= ~intersection; // F:[CM-33]
tokensToDisable &= ~intersection; // F:[CM-33]

// check that operation doesn't try to enable one of forbidden tokens
if (forbiddenTokenMask & tokensToEnable != 0) {
revert TokenNotAllowedException(); // F:[CM-30,32]
}
// // check that operation doesn't try to enable one of forbidden tokens
// if (forbiddenTokenMask & tokensToEnable != 0) {
// revert TokenNotAllowedException(); // F:[CM-30,32]
// }

uint256 enabledTokens = enabledTokensMap[creditAccount];

Expand Down Expand Up @@ -1050,12 +1033,6 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
poolQuotaKeeper().updateQuotas(creditAccount, quotaUpdates, enabledTokensMap[creditAccount]); // F: [CMQ-3]

cumulativeQuotaInterest[creditAccount] += caInterestChange; // F: [CMQ-3]

uint256 totalTokensEnabled = _calcEnabledTokens(enabledTokensMask);
if (totalTokensEnabled > maxAllowedEnabledTokenLength) {
revert TooManyEnabledTokensException(); // F: [CMQ-11]
}

enabledTokensMap[creditAccount] = enabledTokensMask; // F: [CMQ-3]
}

Expand Down Expand Up @@ -1342,7 +1319,7 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
uint256 quotaInterest;

if (supportsQuotas) {
TokenLT[] memory tokens = getLimitedTokens(creditAccount);
TokenLT[] memory tokens = getQuotedTokens(creditAccount);

quotaInterest = cumulativeQuotaInterest[creditAccount] - 1;

Expand Down
5 changes: 3 additions & 2 deletions contracts/interfaces/ICreditManagerV2.sol
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ interface ICreditManagerV2 is ICreditManagerV2Events, IVersion {
/// @param collateralHints Array of token masks in the desired order of evaluation
/// @param minHealthFactor Minimal health factor of the account, in PERCENTAGE format
function fullCollateralCheck(address creditAccount, uint256[] memory collateralHints, uint16 minHealthFactor)
external;
external
returns (uint256 enabledTokenMaskAfter);

/// @dev Checks that the number of enabled tokens on a Credit Account
/// does not violate the maximal enabled token limit and tries
Expand Down Expand Up @@ -232,7 +233,7 @@ interface ICreditManagerV2 is ICreditManagerV2Events, IVersion {
returns (address token, uint16 liquidationThreshold);

/// @dev Returns the array of quoted tokens that are enabled on the account
function getLimitedTokens(address creditAccount) external view returns (TokenLT[] memory tokens);
function getQuotedTokens(address creditAccount) external view returns (TokenLT[] memory tokens);

/// @dev Total number of known collateral tokens.
function collateralTokensCount() external view returns (uint256);
Expand Down
4 changes: 3 additions & 1 deletion contracts/interfaces/IExceptions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ error ExpectedBalancesAlreadySetException();

/// @dev Thrown if a Credit Account has enabled forbidden tokens and the owner attempts to perform an action
/// that is not allowed with any forbidden tokens enabled
error ActionProhibitedWithForbiddenTokensException();
error ForbiddenTokensException();

/// @dev Thrown when attempting to perform an action on behalf of a borrower that is blacklisted in the underlying token
error NotAllowedForBlacklistedAddressException();
Expand Down Expand Up @@ -224,3 +224,5 @@ error CreditFacadeNonBlacklistable();
error NothingToClaimException();

error LiquiditySanityCheckException();

error ZeroCallsException();
16 changes: 16 additions & 0 deletions contracts/libraries/TokenIndex.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
// Gearbox Protocol. Generalized leverage for DeFi protocols
// (c) Gearbox Holdings, 2022
pragma solidity ^0.8.17;

/// @title TokenIndex library
library TokenIndex {
function unzip(uint256 value) internal pure returns (address token, uint8 maskIndex) {
maskIndex = uint8(value >> 160);
token = address(uint160(value));
}

function zipWith(address token, uint8 maskIndex) internal pure returns (uint256 result) {
result = uint256(maskIndex) << 160 | uint256(uint160(token));
}
}
Loading

0 comments on commit 59b2779

Please sign in to comment.