Wonderful Chartreuse Poodle
High
The liquidatePosition() function relies on external liquidators to initiate liquidations when a position falls below the required collateral threshold. However, if no one chooses to liquidate, the position could continue to deteriorate, resulting in the protocol accruing bad debt. This makes the lending system unstable and puts lenders at risk. https://github.com/sherlock-audit/2025-02-yieldoor/blob/main/yieldoor/src/Leverager.sol#L299-L379
In liquidatePosition.sol:302, the function relies on external users to trigger liquidation, but does not enforce automatic liquidation when positions fall below the required threshold. https://github.com/sherlock-audit/2025-02-yieldoor/blob/main/yieldoor/src/Leverager.sol#L302
In liquidatePosition.sol:315, even if the collateral value is below the borrowed amount, no mechanism exists to automatically repay the debt if no liquidators step in. https://github.com/sherlock-audit/2025-02-yieldoor/blob/main/yieldoor/src/Leverager.sol#L315
For the vulnerability to occur, the following conditions must be met:
-
A user needs to open a leveraged position, borrowing funds from the lending pool, setting {borrowedAmount} to be greater than {totalValueUSD}.
-
Market conditions must cause the value of the collateral to drop, reducing {totalValueUSD} to less than {borrowedValue} within a short timeframe.
-
No liquidator steps in to close the position, allowing the position to reach bad debt territory.
-
The contract does not have an automated liquidation mechanism, leaving liquidation dependent on external participants.
- Market conditions: The price of the collateral asset needs to drop significantly, reducing the loan-to-value (LTV) ratio.
- Oracle latency or manipulation: The price feed (TWAP or external oracle) fails to update in time or gets manipulated, delaying accurate liquidation triggers.
- Gas constraints: High gas fees discourage liquidators from executing liquidations, causing positions to remain undercollateralized.
- Liquidity crisis: The lending pool lacks enough liquidity for liquidators to close positions efficiently.
- Lack of incentives: No sufficient rewards for liquidators, leading to inactivity despite liquidation thresholds being met.
1️⃣ User Opens a Leveraged Position
A user borrows assets using the contract, setting {borrowedAmount} based on their collateral. This amount is greater than or close to the liquidation threshold. 2️⃣ Market Volatility Causes Collateral to Drop in Value
The value of {totalValueUSD} decreases due to market conditions, pushing the position closer to liquidation. The position becomes undercollateralized, meaning the borrowed amount exceeds the collateral’s worth. 3️⃣ No Liquidators Step In
The contract relies on external liquidators to liquidate bad debt. However, if no incentives exist, no one executes the liquidation. 4️⃣ Position Enters Bad Debt
The contract does not enforce automatic liquidation. Since no one liquidates, the lending pool incurs bad debt, affecting all depositors.
- The lending protocol suffers bad debt, leading to losses for liquidity providers (LPs) and lenders.
- LPs may face an approximate loss of funds, as the unliquidated positions remain in debt without sufficient collateral to cover them.
- The attacker/borrower gains by escaping their obligation to repay, leaving the protocol to absorb the losses.
- If the issue persists, the protocol's solvency is at risk, potentially leading to a cascading liquidation failure.
High Severity
Introduce an automated liquidation mechanism:
- Implement a keeper bot or MEV incentives to trigger liquidation when a position reaches the threshold.
- Allow the protocol itself to liquidate if no external liquidator acts within a set timeframe.
- Introduce progressive penalties to force earlier liquidations.
Fixed Code Implementation Modify liquidatePosition() to include an automated fallback liquidation:
function autoLiquidate(uint256 id) external nonReentrant { require(isLiquidateable(id), "Position is not liquidatable");
Position memory up = positions[id];
uint256 currBIndex = ILendingPool(lendingPool).getCurrentBorrowingIndex(up.denomination);
uint256 owedAmount = up.borrowedAmount * currBIndex / up.borrowedIndex;
// Ensure liquidation happens automatically if no one acts
if (block.timestamp - lastLiquidationAttempt[id] > AUTO_LIQUIDATION_TIME) {
ILiquidationBot(liquidationBot).triggerLiquidation(id);
}
ILendingPool(lendingPool).repay(up.denomination, owedAmount);
delete positions[id];
}