Skip to content

Commit

Permalink
feat: limits zeroed on loss + pqk improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
Van0k committed Mar 22, 2023
1 parent 92b03ee commit aa2c5ae
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 16 deletions.
25 changes: 16 additions & 9 deletions contracts/credit/CreditManager.sol
Original file line number Diff line number Diff line change
Expand Up @@ -295,9 +295,7 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
creditFacadeOnly // F:[CM-2]
returns (uint256 remainingFunds)
{
// If the contract is paused and the payer is the emergency liquidator,
// changes closure action to LIQUIDATE_PAUSED, so that the premium is nullified
// If the payer is not an emergency liquidator, reverts
// If the contract is paused and the payer is not an emergency liquidator, reverts
if (paused() && (closureActionType == ClosureAction.CLOSE_ACCOUNT || !canLiquidateWhilePaused[payer])) {
revert("Pausable: paused");
} // F:[CM-5]
Expand All @@ -320,24 +318,33 @@ contract CreditManager is ICreditManagerV2, ACLNonReentrantTrait {
uint256 profit;
uint256 loss;
uint256 borrowedAmountWithInterest;
uint256 quotaInterest;
TokenLT[] memory tokens;

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

quotaInterest = cumulativeQuotaInterest[creditAccount];
uint256 quotaInterest = cumulativeQuotaInterest[creditAccount];

if (tokens.length > 0) {
quotaInterest += poolQuotaKeeper().closeCreditAccount(creditAccount, tokens); // F: [CMQ-6]
}
}

(borrowedAmount, borrowedAmountWithInterest,) =
_calcCreditAccountAccruedInterest(creditAccount, quotaInterest); // F:
(borrowedAmount, borrowedAmountWithInterest,) =
_calcCreditAccountAccruedInterest(creditAccount, quotaInterest); // F: [CMQ-6]
} else {
(borrowedAmount, borrowedAmountWithInterest,) = _calcCreditAccountAccruedInterest(creditAccount, 0); // F: [CMQ-6]
}

(amountToPool, remainingFunds, profit, loss) =
calcClosePayments(totalValue, closureActionType, borrowedAmount, borrowedAmountWithInterest); // F:[CM-10,11,12]

// If there is loss, quota limits for assets on the account are set to 0 automatically
// This prevents further exposure to the tokens, since the loss may have occured due to
// an exploit or price feed deviation
if (supportsQuotas && loss > 0) {
poolQuotaKeeper().setLimitsToZero(tokens); // F: [CMQ-12]
}

uint256 underlyingBalance = IERC20(underlying).balanceOf(creditAccount);

// If there is an underlying surplus, transfers it to the "to" address
Expand Down
4 changes: 4 additions & 0 deletions contracts/interfaces/IPoolQuotaKeeper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@ interface IPoolQuotaKeeper is IPoolQuotaKeeperEvents, IPoolQuotaKeeperExceptions
/// @param tokensLT Array of all active quoted tokens on the account
function closeCreditAccount(address creditAccount, TokenLT[] memory tokensLT) external returns (uint256);

/// @dev Sets limits for a number of tokens to zero, preventing further quota increases
/// @notice Triggered by the Credit Manager when there is loss during liquidation
function setLimitsToZero(TokenLT[] memory tokensLT) external;

/// @dev Computes the accrued quota interest and updates interest indexes
/// @param creditAccount Address of the Credit Account to accrue interest for
/// @param tokensLT Array of all active quoted tokens on the account
Expand Down
30 changes: 24 additions & 6 deletions contracts/pool/PoolQuotaKeeper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait {
/// @dev Timestamp of the last time quota rates were batch-updated
uint256 lastQuotaRateUpdate;

/// @dev Mapping from (user, token) to per-account quota parameters
/// @dev Mapping from (credit manager, user, token) to per-account quota parameters
mapping(address => mapping(address => mapping(address => AccountQuota))) internal quotas;

/// @dev Address of the gauge that determines quota rates
Expand Down Expand Up @@ -199,7 +199,9 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait {
int128 quotaRevenueChange;

uint256 len = tokensLT.length;
for (uint256 i; i < len;) {
uint256 i;

while (i < len && tokensLT[i].token != address(0)) {
address token = tokensLT[i].token;

(int128 qic, uint256 caqi) = _removeQuota(msg.sender, creditAccount, token); // F: [CMQ-06]
Expand All @@ -211,7 +213,6 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait {
}
}

/// TODO: check side effect of updating expectedLiquidity
pool.changeQuotaRevenue(quotaRevenueChange);
}

Expand All @@ -236,6 +237,22 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait {
return (-int128(uint128(quoted)) * int16(tq.rate), caQuotaInterestChange); // F: [CMQ-06]
}

/// @dev Sets limits for a number of tokens to zero, preventing further quota increases
/// @notice Triggered by the Credit Manager when there is loss during liquidation
function setLimitsToZero(TokenLT[] memory tokensLT) external creditManagerOnly {
uint256 len = tokensLT.length;
uint256 i;

while (i < len && tokensLT[i].token != address(0)) {
address token = tokensLT[i].token;
totalQuotas[token].limit = 1; // F: [CMQ-12]

unchecked {
++i;
}
}
}

/// @dev Computes the accrued quota interest and updates interest indexes
/// @param creditAccount Address of the Credit Account to accrue interest for
/// @param tokensLT Array of all active quoted tokens on the account
Expand All @@ -246,8 +263,9 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait {
returns (uint256 caQuotaInterestChange)
{
uint256 len = tokensLT.length;
uint256 i;

for (uint256 i; i < len;) {
while (i < len && tokensLT[i].token != address(0)) {
address token = tokensLT[i].token;
AccountQuota storage q = quotas[msg.sender][creditAccount][token];

Expand Down Expand Up @@ -325,8 +343,9 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait {
returns (uint256 caQuotaInterestChange)
{
uint256 len = tokensLT.length;
uint256 i;

for (uint256 i; i < len;) {
while (i < len && tokensLT[i].token != address(0)) {
address token = tokensLT[i].token;
AccountQuota storage q = quotas[creditManager][creditAccount][token];

Expand Down Expand Up @@ -360,7 +379,6 @@ contract PoolQuotaKeeper is IPoolQuotaKeeper, ACLNonReentrantTrait {
TokenLT[] memory tokens
) external view override returns (uint256 value, uint256 totalQuotaInterest) {
uint256 i;

uint256 len = tokens.length;
while (i < len && tokens[i].token != address(0)) {
(uint256 currentUSD, uint256 outstandingInterest) =
Expand Down
42 changes: 41 additions & 1 deletion contracts/test/credit/CreditManager_Quotas.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -575,7 +575,7 @@ contract CreditManagerQuotasTest is
assertLe(diff, 2, "Total debt not equal");
}

/// @dev [CMQ-11] updateQuotas reverts on too many enabled tokens
/// @dev [CMQ-11]: updateQuotas reverts on too many enabled tokens
function test_CMQ_11_updateQuotas_reverts_on_too_many_tokens_enabled() public {
(,,, address creditAccount) = _openCreditAccount();

Expand All @@ -586,4 +586,44 @@ contract CreditManagerQuotasTest is
evm.expectRevert(TooManyEnabledTokensException.selector);
creditManager.updateQuotas(creditAccount, quotaUpdates);
}

/// @dev [CMQ-12]: Credit Manager zeroes limits on quoted tokens upon incurring a loss
function test_CMQ_12_creditManager_triggers_limit_zeroing_on_loss() public {
_makeTokenLimited(tokenTestSuite.addressOf(Tokens.USDT), 500, uint96(1_000_000 * WAD));

(,,, address creditAccount) = _openCreditAccount();

QuotaUpdate[] memory quotaUpdates = new QuotaUpdate[](2);

quotaUpdates[0] =
QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.LINK), quotaChange: int96(uint96(100 * WAD))});
quotaUpdates[1] =
QuotaUpdate({token: tokenTestSuite.addressOf(Tokens.USDT), quotaChange: int96(uint96(200 * WAD))});

creditManager.updateQuotas(creditAccount, quotaUpdates);

TokenLT[] memory quotedTokens = new TokenLT[](creditManager.maxAllowedEnabledTokenLength() + 1);

quotedTokens[1] = TokenLT({
token: tokenTestSuite.addressOf(Tokens.LINK),
lt: creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.LINK))
});

quotedTokens[0] = TokenLT({
token: tokenTestSuite.addressOf(Tokens.USDT),
lt: creditManager.liquidationThresholds(tokenTestSuite.addressOf(Tokens.USDT))
});

evm.expectCall(address(poolQuotaKeeper), abi.encodeCall(IPoolQuotaKeeper.setLimitsToZero, (quotedTokens)));

creditManager.closeCreditAccount(USER, ClosureAction.LIQUIDATE_ACCOUNT, 0, USER, USER, 0, false);

for (uint256 i = 0; i < quotedTokens.length; ++i) {
if (quotedTokens[i].token == address(0)) continue;

(, uint96 limit,,) = poolQuotaKeeper.totalQuotas(quotedTokens[i].token);

assertEq(limit, 1, "Limit was not zeroed");
}
}
}

0 comments on commit aa2c5ae

Please sign in to comment.