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.