kickWithDeposit removes the deposit without HTP pool state check
Summary
In order to cover kick bond KickerActions kickWithDeposit() removes the deposit from the pool, but misses the new_LUP >= HTP check, allowing for the invariant breaking state.
Vulnerability Detail
Every deposit removal in the protocol comes with the LUP >= HTP final state check, that ensures that active loans aren't eligible for liquidation (Ajna white paper 4.1 Deposit).
kickWithDeposit() can effectively remove deposits, either partially or fully, but performs no such check, potentially leaving the pool in the LUP < HTP state.
Impact
A range of outcomes becomes possible after that, for example all other deposit operations can be frozen as long as they will not move LUP in the opposite direction, as their HTP checks will revert.
There is no low-probability prerequisites and the impact is a violation of the core system invariant, so setting the severity to be high.
Code Snippet
kickWithDeposit() can effectively remove quote tokens from any bucket to cover kick bond:
function kickWithDeposit(
...
) external returns (
KickResult memory kickResult_
) {
...
// kick top borrower
kickResult_ = _kick(
...
);
...
// remove amount from deposits
if (vars.amountToDebitFromDeposit == vars.bucketDeposit && vars.bucketCollateral == 0) {
// In this case we are redeeming the entire bucket exactly, and need to ensure bucket LP are set to 0
vars.redeemedLP = vars.bucketLP;
>> Deposits.unscaledRemove(deposits_, index_, vars.bucketUnscaledDeposit);
vars.bucketUnscaledDeposit = 0;
} else {
...
>> Deposits.unscaledRemove(deposits_, index_, unscaledAmountToRemove);
vars.bucketUnscaledDeposit -= unscaledAmountToRemove;
}
lup_ = Deposits.getLup(deposits_, poolState_.debt);
uint256 htp = Maths.wmul(params_.thresholdPrice, poolState_.inflator);
if (
// check loan book's htp doesn't exceed new lup
htp > lup_
||
// ensure that pool debt < deposits after removal
// this can happen if lup and htp are less than min bucket price and htp > lup (since LUP is capped at min bucket price)
(poolState_.debt != 0 && poolState_.debt > Deposits.treeSum(deposits_))
) revert LUPBelowHTP();
hyh
high
kickWithDeposit removes the deposit without HTP pool state check
Summary
In order to cover kick bond KickerActions kickWithDeposit() removes the deposit from the pool, but misses the
new_LUP >= HTP
check, allowing for the invariant breaking state.Vulnerability Detail
Every deposit removal in the protocol comes with the
LUP >= HTP
final state check, that ensures that active loans aren't eligible for liquidation (Ajna white paper4.1 Deposit
).kickWithDeposit() can effectively remove deposits, either partially or fully, but performs no such check, potentially leaving the pool in the
LUP < HTP
state.Impact
A range of outcomes becomes possible after that, for example all other deposit operations can be frozen as long as they will not move LUP in the opposite direction, as their HTP checks will revert.
There is no low-probability prerequisites and the impact is a violation of the core system invariant, so setting the severity to be high.
Code Snippet
kickWithDeposit() can effectively remove quote tokens from any bucket to cover kick bond:
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/base/Pool.sol#L321-L336
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L149-L243
But there is no HTP check:
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/libraries/external/KickerActions.sol#L242-L273
Tool used
Manual Review
Recommendation
Consider checking
LUP >= HTP
condition in the final state of the operation, similarly to other functions, for example removeQuoteToken():https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L413-L424