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

Unfair Reward Distribution Vulnerability in Staking.sol

Summary

The Staking contract (Staking.sol) is vulnerable to an Unfair Reward Distribution issue, which results in an inequitable distribution of rewards for stake token holders.

Vulnerability Details

The vulnerability is caused by the lack of time constraints in reward share calculation within the update function. This allows malicious users to quickly stake, claim rewards, and withdraw, gaining the same benefits as those who staked for a more extended period.

function update() public {
uint256 totalSupply = TKN.balanceOf(address(this));
if (totalSupply > 0) {
uint256 _balance = WETH.balanceOf(address(this));
if (_balance > balance) {
uint256 _diff = _balance - balance;
if (_diff > 0) {
// @audit reward shares calculation lacks time constraints
uint256 _ratio = _diff * 1e18 / totalSupply;
if (_ratio > 0) {
index = index + _ratio;
balance = _balance;
}
}
}
}
}

Impact

The vulnerability leads to an unfair reward distribution system, as users who stake for longer periods receive the same rewards as those who stake for shorter durations. This disincentivizes users from keeping their tokens staked for extended periods and encourages frequent staking and withdrawal to maximize rewards. As a result, the protocol experiences WETH losses due to frequent withdrawals and fails to attract long-term commitment from users.

POC

This POC demonstrates how staker2, who joins later, would be able to claim the same rewards as staker1can claim the same rewards as staker1, who staked much earlier, leading to an unfair reward distribution scenario.

// File: test/Staking.t.sol
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/Staking.sol";
import {TERC20} from "./Lender.t.sol";
contract StakingTest is Test {
Staking public stake;
TERC20 public WETH;
TERC20 public TKN;
address public staker1 = address(0x1);
address public staker2 = address(0x2);
address public protocol = address(0x3);
function setUp() public {
WETH = new TERC20();
TKN = new TERC20();
stake = new Staking(address(TKN), address(WETH));
WETH.mint(protocol, 1000e18);
}
function test_frontrunReward() public {
TKN.mint(staker1, 1e18);
TKN.mint(staker2, 1e18);
uint8 initialBlock = 100;
vm.roll(initialBlock);
// Staker 1 deposit
vm.startPrank(staker1);
TKN.approve(address(stake), 1e18);
stake.deposit(1e18);
assertEq(stake.balances(staker1), 1e18, "User 1 failed to stake");
vm.stopPrank();
// Staker 1 has staked his fund for awhile
vm.roll(initialBlock + 1000);
// Staker 2 frontrun a deposit after sniffing WETH will be supplied as rewards
vm.startPrank(staker2);
TKN.approve(address(stake), 1e18);
stake.deposit(1e18);
assertEq(stake.balances(staker2), 1e18, "User 2 failed to stake");
vm.stopPrank();
// WETH is supplied & the index is updated by protocol
vm.startPrank(protocol);
WETH.transfer(address(stake), 1e18);
stake.update();
assertEq(stake.balance(), 1e18, "WETH is not supplied");
vm.stopPrank();
// Staker 2 claims rewards and exits his right after
vm.startPrank(staker2);
stake.claim();
uint256 staker2Balance = stake.balances(staker2);
stake.withdraw(staker2Balance);
assertEq(TKN.balanceOf(staker2), staker2Balance, "Withdraw failed");
vm.stopPrank();
vm.startPrank(staker1);
stake.claim();
vm.stopPrank();
// Compare rewards obtained by stakers. Eventhough staker1 has staked a long period before staker2 joined, they both claimed the same amount which is not fair for the one early. More over, staker2 can just come right before reward distribution and enjoy the same benefit as ones before him. Protocol loses reward funds without attracting commitment.
assertTrue(WETH.balanceOf(staker1) > 0, "Reward1 not claimed");
assertTrue(WETH.balanceOf(staker2) > 0, "Reward2 not claimed");
assertEq(WETH.balanceOf(staker1), WETH.balanceOf(staker2), "Reward1 & Reward2 not equal");
}
}

Tools Used

VsCode

Recommendations

To address the unfair reward distribution, the contract should encourage users to lock up their funds for a certain period. This can be achieved by calculating the reward rate (rewardRate) based on the ratio and reward duration. Users' indexes should be calculated proportionally to their staking period, rewarding those who stake for longer periods accordingly.

Support

FAQs

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

Give us feedback!