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.
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)
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))
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);
}