Liquid Staking

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

Operators lose all staked LST in OperatorStakingPool

Summary

Node operators are required to stake their LSTs into OperatorStakingPool contract. However, the operators are unable to withdraw the staked funds from contract. The flow of removing operators also has the same issue.

Vulnerability Details

The function OperatorStakingPool#withdraw() is expected to allow node operators to withdraw staked LST from the contract. However, after internal accounting is done in internal function _withdraw, the contract does not call LST contract to transfer tokens. The issue causes node operators lose all staked funds from contracts.

The function OperatorStakingPool#removeOperators() also has the same issue that call to internal function _withdraw()

PoC

Update the test withdraw should work correctly in test file operator-staking-pool.test.ts as below:

it.only('withdraw should work correctly', async () => {
const { signers, accounts, opPool, lst } = await loadFixture(deployFixture)
await lst.transferAndCall(opPool.target, toEther(1000), '0x')
await lst.connect(signers[1]).transferAndCall(opPool.target, toEther(500), '0x')
await expect(opPool.connect(signers[3]).withdraw(toEther(100))).to.be.revertedWithCustomError(
opPool,
'SenderNotAuthorized()'
)
await opPool.withdraw(toEther(1000))
await opPool.connect(signers[1]).withdraw(toEther(200))
assert.equal(fromEther(await opPool.getOperatorPrincipal(accounts[0])), 0)
assert.equal(fromEther(await opPool.getOperatorStaked(accounts[0])), 0)
assert.equal(fromEther(await opPool.getOperatorPrincipal(accounts[1])), 300)
assert.equal(fromEther(await opPool.getOperatorStaked(accounts[1])), 300)
assert.equal(fromEther(await opPool.getTotalPrincipal()), 300)
assert.equal(fromEther(await opPool.getTotalStaked()), 300)
await lst.setMultiplierBasisPoints(20000)
let balanceBefore = await lst.balanceOf(signers[1].address);
await opPool.connect(signers[1]).withdraw(toEther(500))
assert.equal(fromEther(await opPool.getOperatorPrincipal(accounts[0])), 0)
assert.equal(fromEther(await opPool.getOperatorStaked(accounts[0])), 0)
assert.equal(fromEther(await opPool.getOperatorPrincipal(accounts[1])), 100)
assert.equal(fromEther(await opPool.getOperatorStaked(accounts[1])), 100)
assert.equal(fromEther(await opPool.getTotalPrincipal()), 100)
assert.equal(fromEther(await opPool.getTotalStaked()), 100)
let balanceAfter = await lst.balanceOf(signers[1].address);
assert.notEqual(balanceAfter, balanceBefore)
})

Run the test and it failed.
It means that the LST balance of the operator does not change

Impact

Operators lose all staked LST in OperatorStakingPool

Tools Used

Manual

Recommendations

Add logic to call LST contract to transfer token

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.