Description
PriorityPool::withdraw()
allows users to withdraw their tokens.
When there are enough tokens in the queue, it directly provides those for the unstake.
However, it does not call StakingPool::withdraw
, leading to no tokens being burned and StakingPool::totalStake
not decreasing.
function withdraw(
uint256 _amountToWithdraw,
uint256 _amount,
uint256 _sharesAmount,
bytes32[] calldata _merkleProof,
bool _shouldUnqueue,
bool _shouldQueueWithdrawal
) external {
if (_amountToWithdraw == 0) revert InvalidAmount();
uint256 toWithdraw = _amountToWithdraw;
address account = msg.sender;
if (_shouldUnqueue == true) {
_requireNotPaused();
if (_merkleProof.length != 0) {
bytes32 node = keccak256(
bytes.concat(keccak256(abi.encode(account, _amount, _sharesAmount)))
);
if (!MerkleProofUpgradeable.verify(_merkleProof, merkleRoot, node))
revert InvalidProof();
} else if (accountIndexes[account] < merkleTreeSize) {
revert InvalidProof();
}
uint256 queuedTokens = getQueuedTokens(account, _amount);
uint256 canUnqueue = queuedTokens <= totalQueued ? queuedTokens : totalQueued;
uint256 amountToUnqueue = toWithdraw <= canUnqueue ? toWithdraw : canUnqueue;
if (amountToUnqueue != 0) {
accountQueuedTokens[account] -= amountToUnqueue;
totalQueued -= amountToUnqueue;
toWithdraw -= amountToUnqueue;
emit UnqueueTokens(account, amountToUnqueue);
}
}
if (toWithdraw != 0) {
@> IERC20Upgradeable(address(stakingPool)).safeTransferFrom(
@> account,
@> address(this),
@> toWithdraw
@> );
@> toWithdraw = _withdraw(account, toWithdraw, _shouldQueueWithdrawal);
}
@> token.safeTransfer(account, _amountToWithdraw - toWithdraw);
}
For every withdraw taking the queued tokens, it will increase LST supply and StakingPool::totalStake
, leading to DoS StakingPool::canDeposit()
, preventing the protocol from staking new tokens, even vaults are not full.
function getMaxDeposits() public view returns (uint256) {
uint256 max;
for (uint256 i = 0; i < strategies.length; i++) {
uint strategyMax = IStrategy(strategies[i]).getMaxDeposits();
if (strategyMax >= type(uint256).max - max) {
return type(uint256).max;
}
max += strategyMax;
}
return max;
}
function canDeposit() external view returns (uint256) {
@> uint256 max = getMaxDeposits();
@> if (max <= totalStaked) {
@> return 0;
} else {
return max - totalStaked;
}
}
Risk
Likelyhood: High
Impact: High
Proof of Concept
function testWithdrawPriorityPoolBurnLstToken() public {
linkToken.transfer(alice, 15000*8 ether);
vm.startPrank(alice);
linkToken.approve(address(priorityPool), 15000*8 ether);
linkToken.approve(address(stakingPool), 15000*8 ether);
stakingPool.approve(address(priorityPool), 15000*8 ether);
bytes[] memory strategyData = new bytes[]();
strategyData[0] = abi.encode(uint64(0));
strategyData[1] = abi.encode(uint64(1));
strategyData[2] = abi.encode(uint64(2));
strategyData[3] = abi.encode(uint64(3));
strategyData[4] = abi.encode(uint64(4));
strategyData[5] = abi.encode(uint64(5));
priorityPool.deposit(15000*7 ether, true, strategyData);
uint256 initialTotalStakedAfterDeposit = stakingPool.totalStaked();
uint256 sharesAmount = stakingPool.getSharesByStake(15000 ether);
bytes32[] memory emptyProof = new bytes32[]();
emptyProof[0] = bytes32(0);
uint256 withdrawAmount = 10000 ether;
priorityPool.withdraw(withdrawAmount, 0, sharesAmount, emptyProof, false, true);
vm.stopPrank();
uint256 stakingPoolStakedAfterWithdraw = stakingPool.totalStaked();
assertEq(stakingPoolStakedAfterWithdraw, initialTotalStakedAfterDeposit - withdrawAmount);
}
Recommended Mitigation
Call StakingPool::withdraw
when queued tokens are withdrawn to update totalStake and burn LST tokens.