The Standard

The Standard
DeFiHardhat
20,000 USDC
View results
Submission Details
Severity: low
Invalid

Unfair Reward Distribution

Summary

A single stake Position can have arbitrary amounts of both parts TST and/or EUROs. When returning a stakes "value" with stake(), it returns whichever token TST or EUROs has the smaller balance of tokens for the staker. The staking rewards are determined by the stakers _portion, which is just the split based on their personal stake amount versus the total staked amount.

Because the stake() function returns the smaller balance between TST and EUROs for stakers, and has no relation to the USD amount staked, the rewards for stakers are never distributed fairly based on how much USD the user staked.

Generally speaking, because TST ($0.005?) is much cheaper than EUROs ($1), most stakes will have more TST than EUROs. This can create massive underpayment for people who afk stake TST, if just a single or few users stake some EUROs. This incentivizes people to stake EUROs, but heavily penalizes people who just want to stake TST.

There is an infinite cycle of arbitragers attempting to squeeze out uneven rewards based on raw USD staked. If they fail to any degree then the average user who is staking "unoptimally" will always get the short end of the stick. Consider if the benefits of an infinite arbitrage race outweighs the alternative of having exact USD amounts rewarded exactly proportionally. The arbitrage is completely unnecessary unless it's somehow tying into other aspects or incentives of the protocol. When a user interacts with a staking mechanism, the default assumption is that their USD value of their stake gets rewarded exactly proportionally to all the other stakes.

Vulnerability Details

  1. Imagine there are 101 Position's that stakers have. 100 of them have 1M TST and 1 EUROs, and one has 101 TST and 100 EUROs. A rough case like this happens if you have a lot of lazy TST whale stakers who accumulate EUROs in their position only from distributeFees() rewards, versus someone who manually decides to stake 100 EUROs.

  2. Liquidation happens and LiquidationPool::distributeAssets() is called. This starts a loop to determine every holder's split of the profits from the liquidation, for each collateral asset that was in the vault. Calculation of reward splits: uint256 _portion = asset.amount * _positionStake / stakeTotal;

  3. Rewards from 1M TST and 1 EUROs stake: LiquidationPool::getStakeTotal() is called which returns the sum of all the EUROs token amounts for every position because they are always the smaller amount, so it returns 100 + 100 = 200e18 EUROs. Then LiquidationPool::stake() is called, and returns the user's 1e18 token of EUROs as their stake. This user's cut (_portion) is 1/200 or 0.5%.

  4. Rewards from 101 TST and 100 EUROs stake: LiquidationPool::getStakeTotal() is called, returns 200e18 EUROs. Then LiquidationPool:stake() is called, and returns the user's 100e18 EUROs as their stake. This user's cut is 100/200 or 50%.

  5. Note: The asset.amount is just an irrelevant magnitude because it's the same in both cases. Assume 10 LINK is the total awards asset.amount being distributed as the current collateral asset to stakers).

  6. The result of rewards incremented by _portion is drastically disproportionate to how much was actually staked.

    • The 1M TST and 1 EUROs staker ($5,001): Rewards are 10 LINK * 0.005 = 0.05 LINK ($1)

    • The 101 TST and 100 EUROs ($100.5): Rewards are 10 LINK * 0.5 = 5 LINK ($100).

    • A $5,000 position is getting about $1 of rewards, whereas a $100 position is getting $100 of rewards, this is disproportionate by 5,000x.

Impact

  • Extremely disproportionate rewards for people prefer to only afk stake TST instead of EUROs.

  • Infinite arbitrage that always gives afk or suboptimal stakers the short end of the stick unless perfectly arbitraged, compared to alternative default of perfectly balanced rewards based on USD values staked.

Tools Used

Manual Review

Recommendations

  • Consider distributing rewards based on the USD value of the position. (Changing LiquidationPool::stake() to return position's USD value?)

  • A mechanism that doesn't punish staking one coin instead of the other and defaults to perfectly balanced rewards vs arbitrage races that are never perfect.

Updates

Lead Judging Commences

hrishibhat Lead Judge over 1 year ago
Submission Judgement Published
Invalidated
Reason: Design choice
Assigned finding tags:

informational/invalid

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.