The issue in the LiquidationPool contract arises when a user's confirmed stake is completely withdrawn using the decreasePosition() function, which results in the user being removed from the holders array through the deletePosition() function. However, if the user still has pending stakes in the pendingStakes array that have not yet been consolidated, this premature removal can lead to several problems.
The deletePosition() function is called within decreasePosition() when a user's confirmed stake in both TST and EUROs tokens becomes zero. The deletePosition() function removes the user's address from the holders array and deletes their Position from the positions mapping.
However, if the user still has pending stakes in the pendingStakes array that have not yet been consolidated into their confirmed position, deleting the user from the holders array could lead to a situation where the user has a stake in the system that is not properly accounted for once the pending stakes are consolidated.
https://github.com/Cyfrin/2023-12-the-standard/blob/91132936cb09ef9bf82f38ab1106346e2ad60f91/contracts/LiquidationPool.sol#L144C5-L162C6
This could happen in the following scenario:
A user has a confirmed position with some amount of TST and EUROs.
The user initiates a new stake, which is added to the pendingStakes array but not immediately to their confirmed position.
Before the pending stake is consolidated (which happens after 1 day), the user decides to withdraw their entire confirmed stake using decreasePosition().
The decreasePosition() function calls deletePosition() because the confirmed stake is now zero, removing the user from the holders array.
When consolidatePendingStakes() is eventually called (e.g., the next day or during another interaction with the contract), the pending stake is added to the user's position, but since the user is no longer in the holders array, their stake may not be properly represented in the system.
If a holder is deleted from the holders list even if they have a stake in the pendingStakes list can be significant and affect multiple functionalities of the LiquidationPool contract.
Incorrect Fee Distribution: The distributeFees() function relies on the holders array to distribute EUROs fees proportionally based on the TST stake each holder has. If a holder is incorrectly removed, they will not receive their rightful share of the fees once their pending stakes are consolidated.
Asset Distribution Issues: The distributeAssets() function also uses the holders array to distribute assets when liquidating positions. If a holder is missing from this array, they will not receive their portion of the assets corresponding to their pending stakes.
Stake Accounting Errors: The getStakeTotal() and getTstTotal() functions iterate over the holders array to calculate the total staked amounts. If a holder with pending stakes is not included, these totals will be incorrect, leading to inaccurate calculations for reward distributions, fee allocations, and more.
Reward Claiming Problems: When claiming rewards through the claimRewards() function, if a holder's address is not in the holders array, they might not be able to claim rewards for pending stakes that have been consolidated after they were removed.
https://www.codehawks.com/contests/clql6lvyu0001mnje1xpqcuvl > Known Issues > LiquidationPool
Let's validate the accuracy of two statements mentioned in the documentation under known issues for certain edge cases.
"position function depends on getTstTotal not being 0. However, if current position has TST, then TSTtotal will never be 0."
Under the assumption that every user is affected by the bug, the getTstTotal() function could potentially return 0 if all users have withdrawn their confirmed TST stakes and only have pending TST stakes that have not yet been consolidated. This would mean that the position() function could incorrectly calculate a user's share of EUROs as zero, since it would be based on a TST total that does not account for pending stakes.
"distributeAssets function requires stakeTotal to be greater than 0, but this will always be the case if any _positionStake > 0."
Similarly, if every user has been removed from the holders array due to the bug, the stakeTotal could also be incorrectly calculated as 0, since it is derived from the confirmed stakes of users in the holders array. If stakeTotal is 0, the distributeAssets() function would not be able to distribute assets correctly, as it relies on a positive stakeTotal to determine each user's share of the assets.
You can see that, both the statements are failing.
Manual Review
The contract should be updated to ensure that a user is only removed from the holders array if they have no confirmed position and no pending stakes. This could be done by modifying the deletePosition() function to check for pending stakes before removing a user from the holders array. Additionally, the logic that handles the consolidation of pending stakes should ensure that the holders array is correctly updated to reflect any users who have pending stakes that are being consolidated.
The contest is live. Earn rewards by submitting a finding.
This is your time to appeal against judgements on your submissions.
Appeals are being carefully reviewed by our judges.