Stake limit is is updated by wrong amount for some cases.
Summary
Value of stake amount can change for some adpaters after calling the _stake function. But currently the value of stake Amount is updated before calling the _stake function which leads to wrong updation of the stake limit.
Vulnerability Detail
Following is prefundedDeposit function
function prefundedDeposit() external nonReentrant onlyTranche returns (uint256, uint256) {
LSTAdapterStorage storage $ = _getStorage();
uint256 bufferEthCache = $.bufferEth; // cache storage reads
uint256 queueEthCache = $.totalQueueEth; // cache storage reads
uint256 assets = IWETH9(WETH).balanceOf(address(this)) - bufferEthCache; // amount of WETH deposited at this time
uint256 shares = previewDeposit(assets);
if (assets == 0) return (0, 0);
if (shares == 0) revert ZeroShares();
// Calculate the target buffer amount considering the user's deposit.
// bufferRatio is defined as the ratio of ETH balance to the total assets in the adapter in ETH.
// Formula:
// desiredBufferRatio = (totalQueueEth + bufferEth + assets - s) / (totalQueueEth + bufferEth + stakedEth + assets)
// Where:
// assets := Amount of ETH the user is depositing
// s := Amount of ETH to stake at this time, s <= bufferEth + assets.
//
// Thus, the formula can be simplified to:
// s = (totalQueueEth + bufferEth + assets) - (totalQueueEth + bufferEth + stakedEth + assets) * desiredBufferRatio
// = (totalQueueEth + bufferEth + assets) - targetBufferEth
//
// Flow:
// If `s` <= 0, don't stake any ETH.
// If `s` < bufferEth + assets, stake `s` amount of ETH.
// If `s` >= bufferEth + assets, all available ETH can be staked in theory.
// However, we cap the stake amount. This is to prevent the buffer from being completely drained.
//
// Let `a` be the available amount of ETH in the buffer after the deposit. `a` is calculated as:
// a = (bufferEth + assets) - s
uint256 targetBufferEth = ((totalAssets() + assets) * $.targetBufferPercentage) / BUFFER_PERCENTAGE_PRECISION;
/// WRITE ///
_mint(msg.sender, shares);
uint256 availableEth = bufferEthCache + assets; // non-zero
// If the buffer is insufficient or staking is paused, doesn't stake any of the deposit
StakeLimitTypes.Data memory data = $.packedStakeLimitData.getStorageStakeLimitStruct();
if (targetBufferEth >= availableEth + queueEthCache || data.isStakingPaused()) {
/// WRITE ///
$.bufferEth = availableEth.toUint128();
return (assets, shares);
}
// Calculate the amount of ETH to stake
uint256 stakeAmount; // can be 0
unchecked {
stakeAmount = availableEth + queueEthCache - targetBufferEth; // non-zero, no underflow
}
// If the calculated stake amount exceeds the available ETH, simply assign the available ETH to the stake amount.
// Possible scenarios:
// - Target buffer percentage was changed to a lower value and there is a large withdrawal request pending.
// - There is a pending withdrawal request and the available ETH are not left in the buffer.
// - There is no pending withdrawal request and the available ETH are not left in the buffer.
if (stakeAmount > availableEth) {
// Note: Admins should be aware of this situation and take action to refill the buffer.
// - Pause staking to prevent further staking until the buffer is refilled
// - Update stake limit to a lower value
// - Increase the target buffer percentage
stakeAmount = availableEth; // All available ETH
}
// If the amount of ETH to stake exceeds the current stake limit, cap the stake amount.
// This is to prevent the buffer from being completely drained. This is not a complete solution.
uint256 currentStakeLimit = StakeLimitUtils.calculateCurrentStakeLimit(data); // can be 0 if the stake limit is exhausted
if (stakeAmount > currentStakeLimit) {
stakeAmount = currentStakeLimit;
}
/// WRITE ///
// Update the stake limit state in the storage
$.packedStakeLimitData.setStorageStakeLimitStruct(data.updatePrevStakeLimit(currentStakeLimit - stakeAmount));
/// INTERACT ///
// Deposit into the yield source
// Actual amount of ETH spent may be less than the requested amount.
stakeAmount = _stake(stakeAmount); // stake amount can be 0
/// WRITE ///
$.bufferEth = (availableEth - stakeAmount).toUint128(); // no underflow theoretically
return (assets, shares);
}
Main lines of code are as follows
/// WRITE ///
// Update the stake limit state in the storage
$.packedStakeLimitData.setStorageStakeLimitStruct(data.updatePrevStakeLimit(currentStakeLimit - stakeAmount));
/// INTERACT ///
// Deposit into the yield source
// Actual amount of ETH spent may be less than the requested amount.
stakeAmount = _stake(stakeAmount); // stake amount can be 0
Now _stake function for example in RsEthAdaptor is as follows
function _stake(uint256 stakeAmount) internal override returns (uint256) {
if (stakeAmount == 0) return 0;
// Check LRTDepositPool stake limit
uint256 stakeLimit = RSETH_DEPOSIT_POOL.getAssetCurrentLimit(Constants.ETH);
if (stakeAmount > stakeLimit) {
// Cap stake amount
stakeAmount = stakeLimit;
}
// Check LRTDepositPool minAmountToDeposit
if (stakeAmount <= RSETH_DEPOSIT_POOL.minAmountToDeposit()) revert MinAmountToDepositError();
// Check paused of LRTDepositPool
if (RSETH_DEPOSIT_POOL.paused()) revert ProtocolPaused();
// Interact
IWETH9(Constants.WETH).withdraw(stakeAmount);
uint256 _rsETHAmt = RSETH.balanceOf(address(this));
RSETH_DEPOSIT_POOL.depositETH{value: stakeAmount}(0, REFERRAL_ID);
_rsETHAmt = RSETH.balanceOf(address(this)) - _rsETHAmt;
if (_rsETHAmt == 0) revert InvariantViolation();
return stakeAmount;
}
from above it can be clearly seen that if the stake amount is greater than the stake limit then the value of stake amount is changed to the value of stake limit thus changing the value of stake amount. Now from this is clear that the stake limit can be reduced by wrong amount if updation happens before calling the _stake function.
Impact
Wrong updation of the stake limit. In fact it reduces the stake amount by a larger amount than the amount staked.
Varun_05
medium
Stake limit is is updated by wrong amount for some cases.
Summary
Value of stake amount can change for some adpaters after calling the _stake function. But currently the value of stake Amount is updated before calling the _stake function which leads to wrong updation of the stake limit.
Vulnerability Detail
Following is prefundedDeposit function
Main lines of code are as follows
Now _stake function for example in RsEthAdaptor is as follows
from above it can be clearly seen that if the stake amount is greater than the stake limit then the value of stake amount is changed to the value of stake limit thus changing the value of stake amount. Now from this is clear that the stake limit can be reduced by wrong amount if updation happens before calling the _stake function.
Impact
Wrong updation of the stake limit. In fact it reduces the stake amount by a larger amount than the amount staked.
Code Snippet
https://github.com/sherlock-audit/2024-05-napier-update/blob/c31af59c6399182fd04b40530d79d98632d2bfa7/napier-uups-adapters/src/adapters/BaseLSTAdapterUpgradeable.sol#L153
Tool used
Manual Review
Recommendation
update the stake limit after calling the _stake function.
Duplicate of #8