Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: low
Invalid

_queueDepositMax and _queueDepositMin can be arbitrarily using through the function depositQueuedTokens() externally, whereas they should only be set by the owner

Summary

function setQueueDepositParams(
uint128 _queueDepositMin,
uint128 _queueDepositMax
) external onlyOwner {
queueDepositMin = _queueDepositMin;
queueDepositMax = _queueDepositMax;
emit SetQueueDepositParams(_queueDepositMin, _queueDepositMax);
}

_queueDepositMin and _queueDepositMax are parameters that should only be modifiable by the owner.

/**
* @notice Deposits queued and/or unused tokens into staking pool strategies
* @dev will revert if less than queueDepositMin tokens can be deposited
* @param _performData encoded list of deposit data to be passed to staking pool strategies (bytes[])
*/
function performUpkeep(bytes calldata _performData) external {
bytes[] memory depositData = abi.decode(_performData, (bytes[]));
_depositQueuedTokens(queueDepositMin, queueDepositMax, depositData);
}

_queueDepositMin and _queueDepositMax are parameters that should only be modifiable by the owner. They are used in the _depositQueuedTokens function as conditions for boundary checks.

function depositQueuedTokens(
uint256 _queueDepositMin,
uint256 _queueDepositMax,
bytes[] calldata _data
) external {
_depositQueuedTokens(_queueDepositMin, _queueDepositMax, _data);
}

However, depositQueuedTokens breaks this security assumption. This function allows users to call it with arbitrary _queueDepositMin and _queueDepositMax values.

Vulnerability Details

function _depositQueuedTokens(
uint256 _depositMin,
uint256 _depositMax,
bytes[] memory _data
) internal {
if (poolStatus != PoolStatus.OPEN) revert DepositsDisabled();
uint256 strategyDepositRoom = stakingPool.getStrategyDepositRoom();
if (strategyDepositRoom == 0 || strategyDepositRoom < _depositMin)
revert InsufficientDepositRoom();
uint256 _totalQueued = totalQueued;
uint256 unusedDeposits = stakingPool.getUnusedDeposits();
uint256 canDeposit = _totalQueued + unusedDeposits;
if (canDeposit == 0 || canDeposit < _depositMin) revert InsufficientQueuedTokens();
uint256 toDepositFromStakingPool = MathUpgradeable.min(
MathUpgradeable.min(unusedDeposits, strategyDepositRoom),
_depositMax
);
uint256 toDepositFromQueue = MathUpgradeable.min(
MathUpgradeable.min(_totalQueued, strategyDepositRoom - toDepositFromStakingPool),
_depositMax - toDepositFromStakingPool
);
stakingPool.deposit(address(this), toDepositFromQueue, _data);
_totalQueued -= toDepositFromQueue;
if (_totalQueued != totalQueued) {
uint256 diff = totalQueued - _totalQueued; //diff == 1
depositsSinceLastUpdate += diff;
sharesSinceLastUpdate += stakingPool.getSharesByStake(diff); //stakingPool.getSharesByStake(diff) == 0
totalQueued = _totalQueued;
}
emit DepositTokens(toDepositFromStakingPool, toDepositFromQueue);
}

Secondly, when _queueDepositMin is set to 1 or lower, an appropriately chosen _queueDepositMax can ultimately result in a diff value of 1. This causes getSharesByStake(1) to return 0, leading to a scenario where sharesSinceLastUpdate remains unchanged while depositsSinceLastUpdate increases by 1.

This mismatch between depositsSinceLastUpdate and sharesSinceLastUpdate results in an incorrect internal state of the contract.

Impact

Bypasses the security assumption
Contract records an incorrect state between depositsSinceLastUpdate and sharesSinceLastUpdate

Recommendations

If this interface is mandatory, basic checks on the parameters are necessary.

Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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