Skip to content
Request an audit

‹ All findings

An attacker may DoS user Fluid balance increases by frontrunning FluidLocker::claim() calls and calling EP_PROGRAM_MANAGER::batchUpdateUserUnits() directly

MediumSuperfluid Locker System·Sherlock · Streaming · 20th November 2024CodebaseM-1

Summary

FluidLocker::claim() connects to the fluid pool and then calls EPProgramManager::batchUpdateUserUnits() to verify the signature and update user points.

However, any attacker may frontrun this call and call directly EPProgramManager::batchUpdateUserUnits(), spending the signature (nonce) and making the claim transaction revert.

As the Fluid balances only increase whenever the user connects to the pool, the user balance will be 0.

An attacker will profit from this because a large tax amount may be coming from a user instantly unlocking or a program being stopped, which transfer immediately funds to the stakers by calling FLUID.distributeToPool(address(this), TAX_DISTRIBUTION_POOL, amount);.

As the user wanted to stake, they needed Fluid balance to do so, but as the attacker DoSed their claim transaction, they will not have balance to stake and will completely miss these large instant funds. Even if users had already staked before, this claim transaction could be increasing their points (by increasing their program points, they could get more Fluid and stake more Fluid to get more staker points), so an attacker profits from not allowing the user to do so just before the tax is distributed.

Note1: the tax is distributed pro-rata to the units of each user, so if the attacker denies a user increasing their funds, the attacker will receive a larger share of the funds.

Root Cause

EP_PROGRAM_MANAGER::updateUserUnits() may be called directly allowing attackers to DoS FluidLocker::claim().

Internal pre-conditions

None.

External pre-conditions

None.

Attack Path

  1. Attacker spots that a user is unlocking or a program is coming to an end and a large sum of tax is coming.
  2. Attacker frontruns users that are going to increase their Fluid balance in the Locker by calling FluidLocker::claim() so that the Locker connects to the program pool and receives the corresponding Fluid tokens.
  3. Due to the FluidLocker::claim() call reverting, users will have less Fluid in the Locker and will stake less funds, getting less points, which means the attacker will get a bigger share of the incoming TAX distribution.

Impact

Attacker profits from having a bigger share of the TAX distributions and users lose this share.

PoC

The only way to collect the Locker to the program pool is by calling FluidLocker::claim() which may be DoSed by calling EP_PROGRAM_MANAGER.updateUserUnits()` directly using the same signature.

solidity
function claim(uint256 programId, uint256 totalProgramUnits, uint256 nonce, bytes memory stackSignature)
    external
    nonReentrant
{
    // Get the corresponding program pool
    ISuperfluidPool programPool = EP_PROGRAM_MANAGER.getProgramPool(programId);

    if (!FLUID.isMemberConnected(address(programPool), address(this))) {
        // Connect this locker to the Program Pool
        FLUID.connectPool(programPool);
    }

    // Request program manager to update this locker's units
    EP_PROGRAM_MANAGER.updateUserUnits(lockerOwner, programId, totalProgramUnits, nonce, stackSignature);

    emit IFluidLocker.FluidStreamClaimed(programId, totalProgramUnits);
}

Mitigation

The locker should have a separate method to connect to the pool.