Liquid Staking

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

New deposit does not work even if there is demand for withdrawal or room for more deposit.

Summary

Vulnerability Details

Lines of impacted code:

function _deposit(
address _account,
uint256 _amount,
bool _shouldQueue,
bytes[] memory _data
) internal {
if (poolStatus != PoolStatus.OPEN) revert DepositsDisabled();
uint256 toDeposit = _amount;
if (totalQueued == 0) {
uint256 queuedWithdrawals = withdrawalPool.getTotalQueuedWithdrawals();
if (queuedWithdrawals != 0) {
uint256 toDepositIntoQueue = toDeposit <= queuedWithdrawals
? toDeposit
: queuedWithdrawals;
withdrawalPool.deposit(toDepositIntoQueue);
toDeposit -= toDepositIntoQueue;
IERC20Upgradeable(address(stakingPool)).safeTransfer(_account, toDepositIntoQueue);
}
if (toDeposit != 0) {
uint256 canDeposit = stakingPool.canDeposit();
if (canDeposit != 0) {
uint256 toDepositIntoPool = toDeposit <= canDeposit ? toDeposit : canDeposit;
stakingPool.deposit(_account, toDepositIntoPool, _data);
toDeposit -= toDepositIntoPool;
}
}
}
.....

Depositing LINK in withdrawal pool or staking pool only happens if totalQueued == 0. Anytime strategy contract owner might increase max deposit limit that will make more room for deposits in staking pool. Depositors might also want to queue LINK withdrawal requests in withdrawal pool. In both cases if new depositors come in, their LINK deposit first should go to withdrawal pool and then rest of the deposit should go to staking pool. But that won't happen if if totalQueued is more than 0.

Proof of Concept

Steps to reproduce:

  1. create a test file inside the test folder of the project directory.

  2. copy and paste the code below.

  3. run it with npx hardhat test test/testing-priority-pool.ts --network hardhat command.

Expected output logs:

Room for deposit in staking pool: 1000
Total queued withdrawals: 100
Total LINK token queued in priority pool: 500

Test Code:

import { assert, expect } from 'chai'
import {
toEther,
deploy,
fromEther,
deployUpgradeable,
getAccounts,
setupToken,
} from './utils/helpers'
import {
ERC677,
SDLPoolMock,
StakingPool,
PriorityPool,
StrategyMock,
WithdrawalPool,
} from '../typechain-types'
import { ethers } from 'hardhat'
import { StandardMerkleTree } from '@openzeppelin/merkle-tree'
import { loadFixture } from '@nomicfoundation/hardhat-network-helpers'
describe('PriorityPool', () => {
async function deployFixture() {
const { accounts, signers } = await getAccounts()
const adrs: any = {}
const token = (await deploy('contracts/core/tokens/base/ERC677.sol:ERC677', [
'Chainlink',
'LINK',
1000000000,
])) as ERC677
adrs.token = await token.getAddress()
await setupToken(token, accounts, true)
const stakingPool = (await deployUpgradeable('StakingPool', [
adrs.token,
'Staked LINK',
'stLINK',
[],
toEther(10000),
])) as StakingPool
adrs.stakingPool = await stakingPool.getAddress()
const strategy = (await deployUpgradeable('StrategyMock', [
adrs.token,
adrs.stakingPool,
toEther(1000),
toEther(100),
])) as StrategyMock
adrs.strategy = await strategy.getAddress()
const sdlPool = (await deploy('SDLPoolMock')) as SDLPoolMock
adrs.sdlPool = await sdlPool.getAddress()
const pp = (await deployUpgradeable('PriorityPool', [
adrs.token,
adrs.stakingPool,
adrs.sdlPool,
toEther(100),
toEther(1000),
])) as PriorityPool
adrs.pp = await pp.getAddress()
const withdrawalPool = (await deployUpgradeable('WithdrawalPool', [
adrs.token,
adrs.stakingPool,
accounts[0],
toEther(10),
0,
])) as WithdrawalPool
adrs.withdrawalPool = await withdrawalPool.getAddress()
await stakingPool.addStrategy(adrs.strategy)
await stakingPool.setPriorityPool(adrs.pp)
await stakingPool.setRebaseController(accounts[0])
await pp.setDistributionOracle(accounts[0])
await pp.setWithdrawalPool(adrs.withdrawalPool)
for (let i = 0; i < signers.length; i++) {
await token.connect(signers[i]).approve(adrs.pp, ethers.MaxUint256)
}
return { signers, accounts, adrs, token, stakingPool, strategy, sdlPool, pp, withdrawalPool }
}
it('deposit does not work even if there is demand for withdrawal or room for deposit.', async () => {
const { signers, accounts, adrs, pp, token, strategy, stakingPool, withdrawalPool } = await loadFixture(
deployFixture
)
await pp.connect(signers[0]).deposit(toEther(1500), true, ['0x'])
assert.equal(fromEther(await pp.totalQueued()), 500)
assert.equal(fromEther(await stakingPool.balanceOf(accounts[0])), 1000)
assert.equal(fromEther(await pp.getQueuedTokens(accounts[0], 0)), 500)
await strategy.setMaxDeposits(toEther(2000)); // making more room for deposit
await stakingPool.connect(signers[0]).approve(adrs.withdrawalPool, ethers.MaxUint256);
await withdrawalPool.connect(signers[0]).queueWithdrawal(accounts[0], toEther(100)); // queuing withdrawal requests
let totalQueuedWithdrawals = await withdrawalPool.getTotalQueuedWithdrawals();
let canDeposit = await stakingPool.canDeposit();
let totalQueued = await pp.totalQueued();
console.log('Room for deposit in staking pool:', fromEther(canDeposit));
console.log('Total queued withdrawals:', fromEther(totalQueuedWithdrawals));
console.log('Total LINK token queued in priority pool:', fromEther(totalQueued));
assert.equal(await token.balanceOf(accounts[2]), toEther(10000))
await pp.connect(signers[2]).deposit(toEther(1000), false, ['0x'])
assert.equal(await token.balanceOf(accounts[2]), toEther(10000)) // Depositor balance remains same
})
})

Impact

  • Deposit might completely fail even if there is room for new deposits.

Tools Used

HardHat

Recommendations

Updates

Lead Judging Commences

inallhonesty Lead Judge
about 1 year ago
inallhonesty Lead Judge about 1 year ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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