20,000 USDC
View results
Submission Details
Severity: high
Valid

Staking rewards do not accrue to depositors

Summary

Users who deposits reward tokens into the Staking contract does not accrue any Staking rewards.

Vunerability Details

After deposit of rewardTokens into the Staking contract, no rewards accrue to the depositor as the condition for claimable reward accrual is never satisfied. Consider the following scenario to understand the issue:

  • Staking contract has 2000 WETH.

  • User deposits 1000 rewardTokens by calling Staking.deposit().

  • Execution enters updateFor(user), which calls update().

  • The stakingIndex (0 to begin with) is incremented by the deposit ratio: uint256 _ratio = _diff * 1e18 / totalSupply and _diff = _balance - balance. This ratio stands at 2e18 as per the calculation .

  • Staking.index is set to 2e18

  • Staking.balance is set to WETH.balanceOf(address(this)) i.e., its own WETH balance.

We continue in updateFor(user):

  • Staking.index is set to 1000e18 (user's staked reward token balance)

  • The differece between userIndex and global index uint256 _delta = index - _supplyIndex; is calculated: User index is 2e18 (as set in update()), as global index is also 2e18, so _delta is 0.

  • Due to 0 delta, no shares accrue to depositor.

The global index value will always be equal to the recipient supplyIndex, leading to uint256 _delta = index - _supplyIndex; always resolving to 0 and no rewards ever accruing to any user.

The only way for rewards to accrue to depositors is if the Staking contract WETH balance increases after the first user deposit. This results in the updateFor()'s _delta calculation to be greater than 0 as the global index becomes greater than the supplier index. Even with this subsequent WETH balance increase in Staking.sol, the rewards accrue only to the subsequent depositors. Those who deposited before won't accrue any rewards.

The following test can be placed in Lender.t.sol to demonstrate the issue:

function testStake() external {
// SETUP
uint DEPOSIT_AMOUNT = 1000e18;
// Deal ETH to user and Staking
vm.deal(alex, 100 ether);
vm.deal(joe, 100 ether);
vm.deal(address(this), 100_000 ether);
vm.deal(address(staking), 100_000 ether * 2);
// Mint RTK
rewardToken.mint(depositor, DEPOSIT_AMOUNT);
rewardToken.mint(alex, DEPOSIT_AMOUNT);
rewardToken.mint(joe, DEPOSIT_AMOUNT);
// SETUP
address interactor = alex;
// Staking deposits WETH
vm.startPrank(address(staking));
weth.deposit{value: DEPOSIT_AMOUNT*2}();
// Prank as user
vm.startPrank(interactor);
// User deposits 1000e18
rewardToken.approve(address(staking), DEPOSIT_AMOUNT);
staking.deposit(DEPOSIT_AMOUNT);
// !!Second WETH deposit into Staking, results in rewards accrual to second depositor
// Uncomment the below two lines to see that rewards do not accrue for second depositor
vm.startPrank(address(staking));
weth.deposit{value: DEPOSIT_AMOUNT}();
// Results
console.log("Acc Rewards before subsequent WETH deposit ", staking.claimable(interactor));
vm.stopPrank();
/// SECOND USER DEPOISTS and ACCRUE SREWARDS
interactor = joe;
//Prank as user
vm.startPrank(interactor);
// User approves staking
rewardToken.approve(address(staking), DEPOSIT_AMOUNT);
// User deposits RTK
staking.deposit(DEPOSIT_AMOUNT);
// REsults
console.log("Acc Rewards after subsequent WETH deposit: ", staking.claimable(interactor));
}

Impact

High

Tools Used

Manual Review

Recommendations

A redesign of the entire staking and rewards system is recommended. One mitigation is to drop the index system and simply calculate depositor claimable rewards based on staking amount and staking period.

Support

FAQs

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