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

Any WETH/fees transferred to Staking contract before there are any stakers are stuck/unclaimable

Summary

The Staking contract does not account for any WETH deposited until the first time deposit() is called. This leads to that WETH being permanently stuck and unclaimable

Vulnerability Details

The accounting of the Staking.sol contract only updates when there is a difference in ETH from the previous balance to new ETH balance.

uint256 _balance = WETH.balanceOf(address(this));
//if more ETH than
if (_balance > balance) {
uint256 _diff = _balance - balance;

However, when WETH is transferred to this contract before there are any stakers, it does not get updated/accounted for. This is because the update() function is first called in the contract after the first deposit() of TKN, but not after WETH is received. This is likely to happen when a fee contract is attached to the staking contract, as the first fee deposit can reasonably come before the first person stakes.

This can be verified by checking that when a deposit is sent to the staking contract after ETH, that the claimable mapping remains zero (you could also check via vm.warp that this is a time-based issue, but its not). We can see in this Proof of Concept that when bob stakes and withdraws, they don't get any of the WETH. In fact, any subsequent stakers seem to be unable to claim that ETH, meaning that the ETH is permanently stuck in the contract. This exact fact is hard to conclusively prove with POC:

// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "forge-std/console.sol";
import {Staking} from "../src/Staking.sol";
import {WETH9} from "../src/test/Weth.sol";
import {TKN} from "../src/test/TKN.sol";
contract StakingTest is Test {
Staking staking;
WETH9 weth;
TKN tkn;
//users:
//This contract is owner
address attacker = address(1);
address bob = address(2);
address test_address = address(this);
function setUp() public {
weth = new WETH9();
tkn = new TKN();
staking = new Staking(address(tkn), address(weth));
//get 1000WETH
weth.deposit{value: 1000000}();
weth.transfer(address(staking), 1000);
tkn.transfer(attacker, 1000);
tkn.transfer(bob, 1000);
}
function log_claim() internal {
uint balance_before = weth.balanceOf(address(this));
staking.claim();
uint balance_after = weth.balanceOf(address(this));
console.log(balance_after- balance_before, "claimed");
}
function test_POC_WETH_Stuck() public {
console.log(weth.balanceOf(address(staking)), "initial weth balance");
tkn.approve(address(staking), 1e30);
staking.deposit(200);
vm.startPrank(bob);
tkn.approve(address(staking), 1000);
staking.deposit(100);
staking.deposit(100);
vm.stopPrank();
weth.transfer(address(staking), 800);
log_claim();
staking.withdraw(200);
weth.transfer(address(staking), 69);
vm.startPrank(bob);
uint balance_before = weth.balanceOf(bob);
staking.claim();
uint balance_after = weth.balanceOf(bob);
console.log(balance_after- balance_before, "claimed");
vm.stopPrank();
uint weth_left = weth.balanceOf(address(staking));
assertEq(weth_left,1000);
}
}

Here are the console logs of this POC (forge tets -vvv):

[PASS] test_POC_WETH_Stuck() (gas: 321710)
Logs:
1000 initial weth balance
400 claimed
469 claimed

We see from the console logs Bob got to claim 50% of the 800 WETH that was deposited after he had deposited, because he owns 200/400 or 50% of the total staked TKN. But he gets 0% of the 1000WETH

Then when the other staker withdraws, and there is another WETH deposit. He owns the entire staking pool and there is 1469 WETH. He gets the 100% of the 469 WETH when he calls staking.claim but 0% of the 1000WETH because that is WETH is stuck, due to the bug that WETH deposited before staking is stuck in the contract.

Impact

WETH deposited before staking is permanently stuck.

Tools Used

Foundry testing

Recommendations

Call update() or a variant of that function when WETH is received before the first stake.

Support

FAQs

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

Give us feedback!