Liquid Staking

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

Missing Token Transfer in OperatorStakingPool:_withdraw, Causing Permanent Loss of Funds for Operator

Summary

In the OperatorStakingPool contract, the _withdraw function only deducts the operator's shares balance without transferring the lst tokens back to the operator, leading to a permanent loss of funds.

Vulnerability Details

The _withdraw function decreases the operator's share balance but fails to execute a transfer of lst tokens back to the operator.

  • Found in contracts/linkStaking/OperatorStakingPool.sol at Line 199

199: => function _withdraw(address _operator, uint256 _amount) private {
200: uint256 sharesAmount = lst.getSharesByStake(_amount);
201: shareBalances[_operator] -= sharesAmount;
202: totalShares -= sharesAmount;
203:
204: emit Withdraw(_operator, _amount, sharesAmount);
205: }

POC

Add following POC snippet to test/linkStaking/operator-staking-pool.test.ts

it('withdraw should send lst token back to operator', async () => {
const { signers, accounts, opPool, lst } = await loadFixture(deployFixture)
// Deposit 500
await lst.connect(signers[1]).transferAndCall(opPool.target, toEther(500), '0x')
const operator1LstBalanceBefore = fromEther(await lst.balanceOf(accounts[1]))
const opPoolLstBalanceBefore = fromEther(await lst.balanceOf(opPool.target))
// Withdraw 200
await opPool.connect(signers[1]).withdraw(toEther(200))
assert.equal(fromEther(await opPool.getOperatorPrincipal(accounts[1])), 300)
assert.equal(fromEther(await opPool.getOperatorStaked(accounts[1])), 300)
const operator1LstBalanceAfter = fromEther(await lst.balanceOf(accounts[1]))
const opPoolLstBalanceAfter = fromEther(await lst.balanceOf(opPool.target))
console.log({operator1LstBalanceBefore, operator1LstBalanceAfter, opPoolLstBalanceBefore, opPoolLstBalanceAfter})
assert.equal(operator1LstBalanceAfter, operator1LstBalanceBefore + 200)
assert.equal(opPoolLstBalanceAfter, opPoolLstBalanceBefore - 200)
})

Result shows that the lst tokens remain inside the opPool which means the operator has lost their deposit.

$ npx hardhat test --network hardhat
OperatorStakingPool
{
operator1LstBalanceBefore: 9500,
operator1LstBalanceAfter: 9500,
opPoolLstBalanceBefore: 500,
opPoolLstBalanceAfter: 500
}
1) withdraw should send lst token back to operator
0 passing (1s)
1 failing
1) OperatorStakingPool
withdraw should send lst token back to operator:
AssertionError: expected 9500 to equal 9700
+ expected - actual
-9500
+9700

Impact

The failure to transfer the lst tokens results in a permanent loss of funds locked in the contract, which cannot be accessed or withdrawn by the operator.

Tools Used

Hardhat

Recommendations

To prevent the permanent loss of funds, include a call to transfer the tokens back to the operator. The recommended fix is:

function _withdraw(address _operator, uint256 _amount) private {
uint256 sharesAmount = lst.getSharesByStake(_amount);
shareBalances[_operator] -= sharesAmount;
totalShares -= sharesAmount;
+ lst.transfer(_operator, _amount);
emit Withdraw(_operator, _amount, sharesAmount);
}
Updates

Lead Judging Commences

inallhonesty Lead Judge about 1 year 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.