Liquid Staking

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

Lack of whenNotPaused Modifier on Critical Functions in PriorityPool

Summary

he PriorityPool contract inherits from the Pausable contract, allowing critical contract functions to be paused during unexpected or malicious activities. However, there is a critical oversight in the implementation of the withdraw and claimLSDTokens functions. The whenNotPaused modifier, intended to prevent certain actions when the contract is paused, is not fully applied in these functions.

In the withdraw function, the whenNotPaused modifier is only applied when the condition shouldUnqueue == true is met. This creates a scenario where the contract can still be exploited even when it is paused, provided the malicious user gains access to stakeLink or receiptToken. Similarly, the whenNotPaused modifier is also missing in the claimLSDTokens function, allowing token claims to proceed even during paused states.

Vulnerability Details

The whenNotPaused modifier, which should restrict operations during paused states, is not applied consistently across all critical conditions. Specifically, in the withdraw function, if shouldUnqueue == false, the function bypasses the whenNotPaused protection, and the claimLSDTokens function entirely lacks this modifier.

Impact

Malicious users can withdraw tokens even when the contract is paused. If they gain access to the stakeLink or receiptToken, they can execute the withdrawal without being blocked by the pause functionality. This exposes the contract to potential exploits or unauthorized withdrawals during periods of critical operations or attacks.

Tools Used

Manual Review

Recommendations

Ensure that all critical functions are protected by the whenNotPaused modifier to prevent their execution when the contract is paused.

function withdraw(
uint256 _amountToWithdraw,
uint256 _amount,
uint256 _sharesAmount,
bytes32[] calldata _merkleProof,
bool _shouldUnqueue,
bool _shouldQueueWithdrawal
- ) external {
+ ) external whenNotPaused {
if (_amountToWithdraw == 0) revert InvalidAmount();
uint256 toWithdraw = _amountToWithdraw;
address account = msg.sender;
// attempt to unqueue tokens before withdrawing if flag is set
if (_shouldUnqueue == true) {
_requireNotPaused();
if (_merkleProof.length != 0) {
bytes32 node = keccak256(bytes.concat(keccak256(abi.encode(account, _amount, _sharesAmount))));
if (!MerkleProofUpgradeable.verify(_merkleProof, merkleRoot, node)) {
revert InvalidProof();
}
} else if (accountIndexes[account] < merkleTreeSize) {
revert InvalidProof();
}
uint256 queuedTokens = getQueuedTokens(account, _amount);
uint256 canUnqueue = queuedTokens <= totalQueued ? queuedTokens : totalQueued;
uint256 amountToUnqueue = toWithdraw <= canUnqueue ? toWithdraw : canUnqueue;
if (amountToUnqueue != 0) {
accountQueuedTokens[account] -= amountToUnqueue;
totalQueued -= amountToUnqueue;
toWithdraw -= amountToUnqueue;
emit UnqueueTokens(account, amountToUnqueue);
}
}
// attempt to withdraw if tokens remain after unqueueing
if (toWithdraw != 0) {
IERC20Upgradeable(address(stakingPool)).safeTransferFrom(account, address(this), toWithdraw);
toWithdraw = _withdraw(account, toWithdraw, _shouldQueueWithdrawal);
}
token.safeTransfer(account, _amountToWithdraw - toWithdraw);
}
- function claimLSDTokens(uint256 _amount, uint256 _sharesAmount, bytes32[] calldata _merkleProof) external {
+ function claimLSDTokens(uint256 _amount, uint256 _sharesAmount, bytes32[] calldata _merkleProof) external whenNotPaused {
address account = msg.sender;
bytes32 node = keccak256(bytes.concat(keccak256(abi.encode(account, _amount, _sharesAmount))));
if (!MerkleProofUpgradeable.verify(_merkleProof, merkleRoot, node)) revert InvalidProof();
uint256 amountToClaim = _amount - accountClaimed[account];
uint256 sharesAmountToClaim = _sharesAmount - accountSharesClaimed[account];
uint256 amountToClaimWithYield = stakingPool.getStakeByShares(sharesAmountToClaim);
if (amountToClaimWithYield == 0) revert NothingToClaim();
accountClaimed[account] = _amount;
accountSharesClaimed[account] = _sharesAmount;
IERC20Upgradeable(address(stakingPool)).safeTransfer(account, amountToClaimWithYield);
emit ClaimLSDTokens(account, amountToClaim, amountToClaimWithYield);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge 10 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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