Liquid Staking

Stakelink
DeFiHardhatOracle
50,000 USDC
View results
Submission Details
Severity: high
Valid

OperatorStaking contract receives token but sends out nothing in the withdraw function

Summary

The `OperatorStaking` contract allows operators to deposit tokens but fails to transfer out tokens during the withdrawal process. Although the internal accounting is handled correctly, the actual token transfer is missing, leaving operators unable to retrieve their staked tokens when they initiate a withdrawal.

Vulnerability Details

/**
* @title Operator Staking Pool
* @notice Tracks node operator LST balances for the purpose of differentiating from community LST balances
NOTE >>> * @dev node operators are required to stake their LSTs into this contract
*/

In the `OperatorStaking` contract, deposits are accepted using the `onTokenTransfer` function, which increases the operator's share balances. However, the `_withdraw` function, which is responsible for processing withdrawals, only adjusts the internal accounting for shares and does not perform the actual token transfer. This results in the operator being unable to retrieve their staked tokens from the contract.

Relevant parts of the contract:

1. **Deposit Function** (`onTokenTransfer`):

@audit>>1 . >>. function onTokenTransfer(address _sender, uint256 _value, bytes calldata) external {
if (msg.sender != address(lst)) revert InvalidToken();
if (!isOperator(_sender)) revert SenderNotAuthorized();
if (getOperatorStaked(_sender) + _value > depositLimit) revert ExceedsDepositLimit();
uint256 sharesAmount = lst.getSharesByStake(_value);
shareBalances[_sender] += sharesAmount;
totalShares += sharesAmount;
emit Deposit(_sender, _value, sharesAmount);
}

2. **remove Operators Function** (`_withdraw`):

/**
* @notice Removes existing operators
* @param _operators list of operators to remove
**/
function removeOperators(address[] calldata _operators) external onlyOwner {
uint256 numOperators = operators.length;
for (uint256 i = 0; i < _operators.length; ++i) {
address operator = _operators[i];
if (!isOperator(operator)) revert OperatorNotFound();
uint256 staked = getOperatorStaked(operator);
if (staked != 0) {
@audit>>2. >>. _withdraw(operator, staked);
}

3. **Withdraw Function** (`_withdraw`):

@audit>>3. >>. function _withdraw(address _operator, uint256 _amount) private {
uint256 sharesAmount = lst.getSharesByStake(_amount);
shareBalances[_operator] -= sharesAmount;
totalShares -= sharesAmount;
emit Withdraw(_operator, _amount, sharesAmount);
}

The `_withdraw` function only performs the internal accounting of shares but fails to include the logic for transferring tokens back to the operator, which means operators will never receive their tokens after initiating a withdrawal.

Impact

- **Loss of Withdrawn Funds**: Operators can deposit tokens into the staking pool, but when they attempt to withdraw, the tokens remain locked in the contract. This results in operators permanently losing access to their staked tokens.

Tools Used

Manual review

Recommendations

To fix this issue, the withdrawal function should include a token transfer to return the tokens to the operator.

Updates

Lead Judging Commences

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

`OperatorStakingPool::_withdraw()` function doesn't transfer the tokens

Support

FAQs

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