Core Contracts

Regnum Aurum Acquisition Corp
HardhatReal World AssetsNFT
77,280 USDC
View results
Submission Details
Severity: low
Valid

Unable to deposit/withdraw in some cases due to revert on liquidity rebalance

Vulnerability Details

In LendingPool contract, when deposit and withdraw, it always rebalance after execution:

function deposit(uint256 amount) external nonReentrant whenNotPaused onlyValidAmount(amount) {
// Update the reserve state before the deposit
ReserveLibrary.updateReserveState(reserve, rateData);
// Perform the deposit through ReserveLibrary
uint256 mintedAmount = ReserveLibrary.deposit(reserve, rateData, amount, msg.sender);
// Rebalance liquidity after deposit
_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();
// Update the reserve state before the withdrawal
ReserveLibrary.updateReserveState(reserve, rateData);
// Ensure sufficient liquidity is available
_ensureLiquidity(amount);
// Perform the withdrawal through ReserveLibrary
(uint256 amountWithdrawn, uint256 amountScaled, uint256 amountUnderlying) = ReserveLibrary.withdraw(
reserve, // ReserveData storage
rateData, // ReserveRateData storage
amount, // Amount to withdraw
msg.sender // Recipient
);
// Rebalance liquidity after withdrawal
_rebalanceLiquidity(); // <--
emit Withdraw(msg.sender, amountWithdrawn);
}

It will deposit/withdraw from curve vault depend on current buffer:

function _rebalanceLiquidity() internal {
// if curve vault is not set, do nothing
if (address(curveVault) == address(0)) {
return;
}
uint256 totalDeposits = reserve.totalLiquidity; // Total liquidity in the system
uint256 desiredBuffer = totalDeposits.percentMul(liquidityBufferRatio);
uint256 currentBuffer = IERC20(reserve.reserveAssetAddress).balanceOf(reserve.reserveRTokenAddress); //@ideaz size protocol .. (có buffer hsy, hay protocol nào khác nhỉ)
if (currentBuffer > desiredBuffer) {
uint256 excess = currentBuffer - desiredBuffer;
// Deposit excess into the Curve vault
_depositIntoVault(excess); // <--
} else if (currentBuffer < desiredBuffer) {
uint256 shortage = desiredBuffer - currentBuffer;
// Withdraw shortage from the Curve vault
_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.

Updates

Lead Judging Commences

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool core operations revert if Curve vault is unavailable during rebalancing, even when sufficient liquidity exists in the pool

inallhonesty Lead Judge 7 months ago
Submission Judgement Published
Validated
Assigned finding tags:

LendingPool core operations revert if Curve vault is unavailable during rebalancing, even when sufficient liquidity exists in the pool

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.

Give us feedback!