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

Depositing order logic lead to stuck of fees

Summary

The logic of first transfer the tokens and them update the global state lead to loss of fees in case a new user enters in staking before receive rewards.

Vulnerability Details

If a new user enters the staking contract after it has already received rewards and the contract's balance is updated before the state, the general state will be calculated with incorrect values. This leads to a decrease in rewards per token and causes some tokens to become stuck in the contract.

It almost the same issue of first deposit but it can happens in any deposit time.

POC

In this POC half o rewards got stuck in the contract.

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import {Test, console2} from "forge-std/Test.sol";
import "../src/Staking.sol";
import {ERC20} from "solady/src/tokens/ERC20.sol";
contract TERC20 is ERC20 {
function name() public pure override returns (string memory) {
return "Test ERC20";
}
function symbol() public pure override returns (string memory) {
return "TERC20";
}
function mint(address _to, uint256 _amount) public {
_mint(_to, _amount);
}
}
contract POCTest is Test {
Staking public staking;
TERC20 public beedle;
TERC20 public weth;
address public attacker = address(0x1);
address public staker = address(0x2);
address public staker2 = address(0x3);
function setUp() public {
beedle = new TERC20();
weth = new TERC20();
staking = new Staking(address(beedle), address(weth));
beedle.mint(staker, 10e18);
beedle.mint(staker2, 10e18);
}
function testStuckRewardOnDeposit() public {
//@audit first staker make the deposit and then mint the reward
vm.startPrank(staker);
beedle.approve(address(staking), 10e18);
staking.deposit(10e18);
vm.stopPrank();
//@audit mint reward
weth.mint(address(staking), 1e18);
//@audit after receives rewards another staker enters the pool
vm.startPrank(staker2);
beedle.approve(address(staking), 10e18);
staking.deposit(10e18);
vm.stopPrank();
//@audit all reward should be to sent to first staker and the next rewards will be divided by both
vm.prank(staker);
staking.claim();
//@audit here still some WETH in the contract but the reward should be distributed
console2.log("Staker claim: ", weth.balanceOf(staker));
console2.log("Contract balance: ", weth.balanceOf(address(staking)));
assertGt(weth.balanceOf(address(staking)), 0);
//@audit In case a new reward is sent to this contract
weth.mint(address(staking), 1e18);
//@audit and both decided to claim, there still a reward stuck in the contract
vm.prank(staker);
staking.claim();
vm.prank(staker2);
staking.claim();
console2.log("Staker claim: ", weth.balanceOf(staker));
console2.log("Staker 2 claim: ", weth.balanceOf(staker2));
console2.log("Contract balance: ", weth.balanceOf(address(staking)));
assertGt(weth.balanceOf(address(staking)), 0);
}
}

Impact

Stuck of fees in the Staking contract

Tools Used

Manual Review

Recommendations

It is recommended to first update the global state and transfer the tokens to Staking contract

Support

FAQs

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