Vulnerability Details
In LendingPool contract, when deposit and withdraw, it always rebalance after execution:
function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
ReserveLibrary.updateReserveState(reserve, rateData);
uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
_rebalanceLiquidity();
emit Deposit(msg.sender, amount, mintedAmount);
}
* @notice Allows a user to withdraw reserve assets by burning RTokens
* @param amount The amount of reserve assets to withdraw
*/
function withdraw(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
if (withdrawalsPaused) revert WithdrawalsArePaused();
ReserveLibrary.updateReserveState(reserve, rateData);
_ensureLiquidity(amount);
(uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
reserve,
rateData,
amount,
msg.sender
);
_rebalanceLiquidity();
emit Withdraw(msg.sender, amountWithdrawn);
}
It will deposit/withdraw from curve vault depend on current buffer:
function _rebalanceLiquidity() internal {
if (address(curveVault) == address(0)) {
return;
}
uint256 totalDeposits = reserve.totalLiquidity;
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress);
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
_depositIntoVault(excess);
} else if (currentBuffer < desiredBuffer) {
uint256 shortage = desiredBuffer - currentBuffer;
_withdrawFromVault(shortage);
}
emit LiquidityRebalanced(currentBuffer, totalVaultDeposits);
}
* @notice Internal function to deposit liquidity into the Curve vault
* @param amount The amount to deposit
*/
function _depositIntoVault(uint256 amount) internal {
IERC20(reserve.reserveAssetAddress).approve(address(curveVault), amount);
curveVault.deposit(amount, address(this));
totalVaultDeposits += amount;
}
* @notice Internal function to withdraw liquidity from the Curve vault
* @param amount The amount to withdraw
*/
function _withdrawFromVault(uint256 amount) internal {
curveVault.withdraw(amount, address(this), msg.sender, 0, new address[](0));
totalVaultDeposits -= amount;
}
But in curve vault, it have max deposit and max withdraw: , which is based on multiple factors like deposit_limit_module, withdraw_limit_module, ... And if amount is bigger than maximum amount, it will revert:
def _deposit(recipient: address, assets: uint256, shares: uint256):
"""
Used for `deposit` and `mint` calls to transfer the amount of `asset` to the vault,
issue the corresponding `shares` to the `recipient` and update all needed
vault accounting.
"""
assert assets <= self._max_deposit(recipient), "exceed deposit limit"
. . . . . . .
def _redeem(
sender: address,
receiver: address,
owner: address,
assets: uint256,
shares: uint256,
max_loss: uint256,
strategies: DynArray[address, MAX_QUEUE]
) -> uint256:
. . . . . .
"""
assert receiver != empty(address), "ZERO ADDRESS"
assert shares > 0, "no shares to redeem"
assert assets > 0, "no assets to withdraw"
assert max_loss <= MAX_BPS, "max loss"
# If there is a withdraw limit module, check the max.
withdraw_limit_module: address = self.withdraw_limit_module
if withdraw_limit_module != empty(address):
assert assets <= IWithdrawLimitModule(withdraw_limit_module).available_withdraw_limit(owner, max_loss, strategies), "exceed withdraw limit"
. . . . . .
So if it revert, rebalance will revert, lead to deposit/withdraw revert in that case. The revert in deposit is more dangerous because if user want to repay when they are under liquidation threshold but failed due to revert on deposit
Impact
In some cases, user are not able to deposit token to protocol to repay due to revert on rebalance, lead to unfairly liquidated.
Recommendations
Using try - catch when rebalancing.