Open sherlock-admin opened 1 year ago
As far as I see there still is a surface of performing such bad debt write off protection just before auction becomes clearable, say by front-running the tx that changes the state so it becomes clearable (say tx removes the last piece of collateral and borrower.t0Debt != 0 && borrower.collateral == 0
becomes true). I.e. HPB depositor might monitor the auction and run multiple addQuoteToken()
just before _revertIfAuctionClearable()
starts to trigger for a big chunk of bad debt now auctioned.
Also, why can't attacker preliminary open another 'protection' deposit far from the top (to avoid liquidation debt, as its size can be small it doesn't have to be yield bearing) and use moveQuoteToken()
to populate higher buckets with dust as described in the issue?
For this end the similar logic can be added to moveQuoteToken()
, e.g.:
function moveQuoteToken(
uint256 maxAmount_,
uint256 fromIndex_,
uint256 toIndex_,
uint256 expiry_
) external override nonReentrant returns (uint256 fromBucketLP_, uint256 toBucketLP_, uint256 movedAmount_) {
_revertAfterExpiry(expiry_);
+ _revertIfAuctionClearable(auctions, loans);
Also, why can't attacker preliminary open another 'protection' deposit far from the top (to avoid liquidation debt, as its size can be small it doesn't have to be yield bearing) and use
moveQuoteToken()
to populate higher buckets with dust as described in the issue?For this end the similar logic can be added to
moveQuoteToken()
, e.g.:function moveQuoteToken( uint256 maxAmount_, uint256 fromIndex_, uint256 toIndex_, uint256 expiry_ ) external override nonReentrant returns (uint256 fromBucketLP_, uint256 toBucketLP_, uint256 movedAmount_) { _revertAfterExpiry(expiry_); + _revertIfAuctionClearable(auctions, loans);
PR to add same for move quote token https://github.com/ajna-finance/contracts/pull/919
To prevent the attack during ongoing auction, e.g. in the front running manner as just described, there is a good option of prohibiting the high price deposits on addition and moving:
function addQuoteToken(
uint256 amount_,
uint256 index_,
uint256 expiry_,
bool revertIfBelowLup_
) external override nonReentrant returns (uint256 bucketLP_) {
_revertAfterExpiry(expiry_);
_revertIfAuctionClearable(auctions, loans);
PoolState memory poolState = _accruePoolInterest();
+ _revertIfAboveHeadAuctionPrice(..., index_);
/// @inheritdoc IPoolLenderActions
function moveQuoteToken(
uint256 maxAmount_,
uint256 fromIndex_,
uint256 toIndex_,
uint256 expiry_
) external override nonReentrant returns (uint256 fromBucketLP_, uint256 toBucketLP_, uint256 movedAmount_) {
_revertAfterExpiry(expiry_);
PoolState memory poolState = _accruePoolInterest();
_revertIfAuctionDebtLocked(deposits, poolState.t0DebtInAuction, fromIndex_, poolState.inflator);
+ _revertIfAboveHeadAuctionPrice(..., toIndex_);
_revertIfAboveHeadAuctionPrice()
is a new control function:
function _revertIfAboveHeadAuctionPrice(
...
uint256 index_
) view {
address head = auctions_.head;
uint256 kickTime = auctions_.liquidations[head].kickTime;
...
if (_auctionPrice(momp, NP, kickTime) < _priceAt(index_)) revert PriceTooHigh();
}
As adding deposits above auction price is generally harmful for depositors (they will be a subject to immediate arbitrage), this will also shield against such uninformed user behavior.
PR to add same for move quote token ajna-finance/contracts#919
Looks ok
hyh
high
Debt write off can be prohibited by HPB depositor by continuously allocating settlement blocking dust deposits in the higher buckets
Summary
High price bucket depositor who faces bad debt settlement can add multiple dust quote token deposits to many higher price buckets and stale settlement.
Vulnerability Detail
HPB depositor have incentives to and actually can defend themselves from using their deposits in bad debt write offs by doing multiple dust quote token deposits in vast number of higher price buckets (up to and above current market price). This will stale bad debt settlement: now logic only requires amount to be positive, SettlerActions.sol#L334-L356, and it is possible to add quote token dust, Pool.sol#L146-L166, LenderActions.sol#L148-L157.
The point in doing so is that, having the deposit frozen is better then participate in a write off, which is a direct loss, as:
1) other unaware depositors might come in and free the HPB depositor from liquidation debt participation, possibly taking bad debt damage,
2) the HPB depositor can still bucketTake() as there is no _revertIfAuctionDebtLocked() check. As it will generate collateral instead of quote funds, it might be then retrieved by removeCollateral().
When there is low amount of debt in liquidation, removing this dust deposits is possible, but economically not feasible: despite high price used gas cost far exceeds the profit due to quote amount being too low.
When there is substantial amount of debt in liquidation, direct removal via removeQuoteToken() will be blocked by _revertIfAuctionDebtLocked() control, while
settle() -> settlePoolDebt()
calls will be prohibitively expensive (will go trough all the dust populated buckets) and fruitless (only dust amount will be settled), while the defending HPB depositor can simultaneously add those dust deposits back.Economically the key point here is that incentives of the defending HPB depositor are more substantial (they will suffer principal loss on bad debt settlement) than the incentives of agents who call
settle() -> settlePoolDebt()
(they have their lower bucket deposits temporary frozen and want to free them with settling bad debt with HPB deposit).Impact
HPB depositors can effectively avoid deposit write off for bad debt settlement. I.e. in some situations when HPB depositor is a whale closely monitoring the pool and knowing that his funds are about to be used to cover a substantial amount of bad debt, the cumulative gas costs of the described strategy will be far lower than the gain of having principal funds recovered over time via
takeBucket() -> removeCollateral()
.This will cause bad debt to pile up and stale greater share of the pool. The HPB depositor will eventually profit off from other depositors, who do not actively monitor pool state and over time participate in the bad debt settlements by placing deposits among the dust ones. This will allow the HPB depositor to obtain stable yield all this time, but off load a part of the corresponding risks.
As there is no low-probability prerequisites and the impact is a violation of system design allowing one group of users to profit off another, setting the severity to be high.
Code Snippet
There is no dust control in addQuoteToken():
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/base/Pool.sol#L146-L166
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/libraries/external/LenderActions.sol#L148-L157
Putting dust in lots of higher buckets will freeze the settlement as there no control over amount to operate with on every iteration, while
bucketDepth_
is limited and there is a block gas limit:https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/libraries/external/SettlerActions.sol#L334-L356
The owner of such deposit can still use it for bucketTake() as there is no _revertIfAuctionDebtLocked() check there (which is ok by itself as the operation reduces the liquidation debt):
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L133-L164
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L415-L464
During _rewardBucketTake() the principal quote funds are effectively exchanged with the collateral:
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/libraries/external/TakerActions.sol#L606-L667
So the HPB depositor can remove it (there is no _revertIfAuctionDebtLocked() check for collateral):
https://github.com/sherlock-audit/2023-04-ajna/blob/main/ajna-core/src/ERC20Pool.sol#L318-L335
But this means that there is no downside in doing so, but it is a significant upside in effectively denying the bad debt settlements.
I.e. the HPB depositor will place his deposit high, gain yield, and when his bucket happens to be within liquidation debt place these dust deposits to prevent settlements. Their deposit will be exchangeable to collateral on bucketTake() over a while, and it's still far better situation than taking part in debt write-off.
Tool used
Manual Review
Recommendation
There might be different design approaches to limiting such a strategy. As an example, consider controlling addQuoteToken() for dust (the limit might be a pool parameter set on deployment with the corresponding explanations that it shouldn't be loo low) and/or controlling it for deposit addition to be buckets higher than current HPB when there is a liquidation debt present (this will also shield naive depositors as such deposits can be subject to write offs, which they can be unaware of, i.e. the reward-risk of such action doesn't look good, so it can be prohibited for both reasons).