Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug fix: kickWithDeposit likely push LUP below HTP affecting healthy loans #894

Merged
merged 17 commits into from
Jun 18, 2023

Conversation

grandizzy
Copy link
Contributor

@grandizzy grandizzy commented Jun 14, 2023

Description of change

High level

  • Current approach removes deposits but since it performs a remove-quote-token like action is lacking check to:

    • revert if auction debt locked
    • revert if new LUP below HTP
  • Without these changes, after kicking max loan LUP would likely fall below HTP hence drawing many more loans undercolalteralized / kickable, which is not desired.
    Introducing these changes renders function unusable as in most cases one of the two reverts would kick in.

  • New simplified approach of kick with deposit function (renamed to lenderKick):

    • does not change deposits, hence doesn't move LUP and no remove-quote-token like check required.
    • lender should only prove that they are entitled with enough funds in bucket that, if removed, would draw loan undercollateralized at proposed LUP (hence kickable).
    • since deposits are not changed, the new approach require lender to post bond (similar with regular kick) in order to kick the top loan
    • it simplifies a lot function logic as it only calculates lender's entitled amount, and performs same _kick procedure as regular kick (with entitled amount as additional debt)
  • Additionally, the LUP was not recalculated in order to include additional kick penalty debt.

Description of bug or vulnerability and solution

  • Due to lack of deposit removal check, kickWithDeposit likely push LUP below HTP affecting healthy loans
  • Fixed by:
    • in kickWithDeposit remove logic to remove from deposits
    • revert with PriceBelowLUP if bucket price is lower than current LUP - in this case lender should call removeQuoteToken function
    • calculate lender's entitled amount in bucket (based on their LP) and caped by bucket deposit size
    • revert with InsufficientLiquidity if calculated entitled amount is 0
    • call _kick function passing entitled amount as additional debt to be used when proposed LUP is calculated. Same logic applies, if deposit is not enough to render loan undercollateralized then tx reverts with BorrowerOk error
    • in _kick function recalculate LUP including debt resulted from kick penalty (for both regular kick and kick with deposit actions) - introduced KickResult.poolDebt struct member, returned and used in Pool contract
    • no more RemoveQuoteToken event emitted by kickWithDeposit
  • Tests updates:
    • QT3 invariant updated to reflect that bonds are always guaranteed (previously this invariant failed when using deposits to cover bonds in a pool without enough liquidity)
    • test helper _kickWithDeposit changed to receive only the bond amount (that should be transferred from kicker) and to remove RemoveQuoteToken emit check
    • updated unit tests

Contract size

Pre Change

============ Deployment Bytecode Sizes ============
  ERC721Pool               -  24,384B  (99.21%)
  ERC20Pool                -  23,612B  (96.07%)

Post Change

============ Deployment Bytecode Sizes ============
  ERC721Pool               -  24,497B  (99.67%)
  ERC20Pool                -  23,739B  (96.59%)

Gas usage

Pre Change

| kick                                 | 149467          | 223488 | 221980 | 1034805 | 48000   |
| kickWithDeposit                      | 199590          | 277221 | 273367 | 1000575 | 8000    |

Post Change

| kick                                 | 172049          | 246062 | 244559 | 1057386 | 48000   |
| lenderKick                      | 216107          | 293739 | 289884 | 1018335 | 8000    |


if (bucket.bankruptcyTime < lender.depositTime) vars.lenderLP = lender.lps;
uint256 lenderLP = bucket.bankruptcyTime < lender.depositTime ? lender.lps : 0;
uint256 bucketDeposit = Deposits.valueAt(deposits_, index_);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should add a check that index_ is greater to or equal to the lup here -- otherwise the provisional removal of the deposit would not actually lower the lup. In that case, the lender can just call removeQuoteToken if they want

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

);
// set LUP to current pool value
kickResult_.lup = vars.currentLup;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, this won't be correct, as the pool's debt will increase slightly. I believe to be totally correct we should recompute the LUP here including the kick penalty interest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

makes sense, though should be done for regular kick too, will add this with a commit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@grandizzy grandizzy changed the title Bug fix: kickWithDeposit to not remove any deposit and to require bond to be posted Bug fix: kickWithDeposit should not remove deposits and require bond to be posted Jun 15, 2023
@grandizzy grandizzy changed the title Bug fix: kickWithDeposit should not remove deposits and require bond to be posted Bug fix: kickWithDeposit likely push LUP below HTP affecting collateralized loans Jun 15, 2023
@grandizzy grandizzy changed the title Bug fix: kickWithDeposit likely push LUP below HTP affecting collateralized loans Bug fix: kickWithDeposit likely push LUP below HTP affecting healthy loans Jun 15, 2023
@grandizzy grandizzy marked this pull request as ready for review June 15, 2023 06:47
Copy link
Contributor

@prateek105 prateek105 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! One suggestion to rename kickWithDeposit to kickToRemoveDeposit.

Copy link
Contributor

@ith-harvey ith-harvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, have a comment regarding bankruptcy

vars.bucketUnscaledDeposit = Deposits.unscaledValueAt(deposits_, index_);
vars.bucketScale = Deposits.scale(deposits_, index_);
vars.bucketDeposit = Maths.wmul(vars.bucketUnscaledDeposit, vars.bucketScale);
vars.lenderLP = bucket.bankruptcyTime < lender.depositTime ? lender.lps : 0;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

kickWithDeposit could be called in the same block as bucket bankruptcy. It would use lender.lps even though the bucket is bankrupt. Recommend adding a revert like we have in moveQuoteToken ->

// cannot move in the same block when target bucket becomes insolvent
if (vars.toBucketBankruptcyTime == block.timestamp) revert BucketBankruptcyBlock();`

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if bucket bankruptcy happens in same block but after kick with deposit is called then vars.lenderLP will be 0 making entitled amount to be 0 and tx to revert with InsufficientLiquidity

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking before, what if a bucket bankruptcy occurs in the same block but before kickWithDeposit is called.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think block when kick happens is the constraint here but the block / time of last deposit?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was misinterpretting the logic...

if bucket.bankruptcyTime < lender.depositTime ? lender.lps : 0; -> if the bucket was bankrupted in the same block this would be 0 as it should be.

All good : )

Copy link
Contributor

@ith-harvey ith-harvey left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Copy link
Contributor

@mattcushman mattcushman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lgtm

Copy link
Contributor

@EdNoepel EdNoepel left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not understand the gas numbers in the report. Why is there a kickWithDeposit post-change, and why did gas cost increase?

Otherwise LGTM.

index_,
vars.amountToDebitFromDeposit,
vars.redeemedLP,
kickResult_.lup
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Quite happy to eliminate all this logic.


_assertPool(
PoolParams({
htp: 0,
lup: 0.000000099836282890 * 1e18,
poolSize: 8_002.290256823112434920 * 1e18,
poolSize: 8_083.134379679494060567 * 1e18,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense because bond isn't coming out of deposit. Confirmed old pool size + bond escrowed (line 486) = new pool size.

Copy link
Collaborator

@MikeHathaway MikeHathaway left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@grandizzy
Copy link
Contributor Author

grandizzy commented Jun 18, 2023

I do not understand the gas numbers in the report. Why is there a kickWithDeposit post-change, and why did gas cost increase?

Otherwise LGTM.

This PR also fix the issue where LUP wasn't recalculated after kick (that is with new debt including kick penalty) which is now done in _kick function for both kick and lenderKick functions. Hence gas increasing by additional Fenwick traversal
https://github.com/ajna-finance/contracts/pull/894/files#diff-54056532b4b7aac8940fbec13725e98ceceb358bef02e1285edad2741dff83bdR363

Post kickWithDeposit is an leftover as gas was taken before refactoring function to lenderKick, updated accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants