Due to a flaw in the "staking position management" logic, stakers, after updating their position, may end up in a situation when they are excluded from staking rewards.
One of the core features of the Protocol is a staking mechanism. Users incentivized to stake their TST and EUROs tokens to earn passive income. The treasury for rewards distribution is built of two sources:
Fee, in the form of EUROs tokens, collected from SmartVaultV3 users, when they mint/burn EUROs tokens
ERC20 tokens and ETH received from SmartVaultV3 (auto) liquidations.
Here is how the staking/rewarding process looks like for stakers:
User initiates position creation by calling LiquidationPool.increasePosition
In this function, LiquidationPool creates a PendingStake for user (line 140), which can be migrated to a "mature" Position after a delay (1d). A PendingStake will start collecting the first type of fees(EUROs) immediately, but ERC20 tokens only after migrating to a Position (this mechanism prevents users from abusing vaults liquidations).
LiquidationPool also adds user's address to the holders array (line 141). This array is used during rewards distribution.
When 1d "timeout" passed, any call to increasePosition, decreasePosition or distributeAssets will trigger consolidatePendingStakes function, and user's PendingStake will be migrated to Position:
At this stage, user have "fully activated" position, and started collection both types of rewards.
At some point, user may decide to close position by calling decreasePosition function. And if user withdrawing all staked assets, the position will be closed by removing corresponding Position, and removing user's address from holders (line 161):
The issue in this "pending-mature position" mechanism is related to the fact that users could have both "pending"(PendingStake) and "mature"(Position) positions, so if a user fully closes his "mature" position, then he gets removed from the holders array. And when his "pending" position migrated to "mature", it will not start collecting rewards, because user is not present in holders, and only holders will be accounted during rewards distribution.
So, user's Position will exist, with user's funds locked, but it will not collect any rewards.
And here is how this scenario can be practically achieved:
User creates PendingSake
24h passed and PendingStake migrated to Position
Some time passed and User decides to increase his deposit, and creates a new PendingStake
24h not yet passed since the last PendingStake, User decides to withdraw his Position, and keep (current) PendingStake (the actual incentives for this decision is related to the specifics of user's strategies, e.g. rebalancing)
24h passed since the last PendigStake created, so it's migrated to Position
Expected state: User's position started collecting rewards
Actual state: User's position excluded from rewards, because user not present in holders
Affected users will miss both types of rewards (most importantly rewards from liquidations) for funds they deposited.
Note that, there also no related events emitted (e.g. position activated) in the current logic, so the user might stay unaware of his position "not yielding" for unknown amount of time.
The following test case implements the scenario described in the previous sections. It shows how a user can end up being excluded from receiving staking rewards.
Please add it to test/liquidationPool.js and run with npx hardhat test.
Note: the patch from the "Recommendation" section fixes the issue and may be used with the same test case to confirm that user should be eligible for rewards.
After reviewing all usages of the holders array in LiquidationPool, it seems like there is no need to update holders in the increasePosition function (which creates PendingStake), because holders is used only to access members of positions mapping.
So we can move logic which updates holders array to consolidatePendingStakes:
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.