Hooks.wtf

New Treasury Managers

StakingManager.sol

[CRITICAL] Missing Reentrancy Protection

The stake(), unstake(), and claim() functions perform external calls but lack reentrancy protection, potentially allowing attackers to manipulate state during external calls.

function stake(uint _amount) external nonReentrant {
  // ..
}

function unstake(uint _amount) external nonReentrant {
  // ..
}

function claim() public nonReentrant returns (uint) {
  // ..
}

[HIGH] Precision Loss in Reward Calculations

Small fee amounts may result in 0 rewards due to precision loss, especially when totalDeposited is large.

function _withdrawFees() internal {
  // ..

  // @audit Check for minimum fee threshold to prevent precision loss
  uint minimumFeeThreshold = totalDeposited / 1e18; // Adjust threshold as needed
  if (availableFees < minimumFeeThreshold) {
    // @audit Skip small fee distributions to prevent precision loss
    return;
  }

  // Update the global ETH rewards per token snapshot
  globalEthRewardsPerTokenX128 += FullMath.mulDiv(availableFees, FixedPoint128.Q128, totalDeposited);
}

[MEDIUM] Division by Zero in balances() Function

The balances() function will revert when totalDeposited is 0, preventing users from checking their balances or claiming rewards. This occurs in the FullMath.mulDiv calculation where totalDeposited is used as the denominator.

function balances(address _recipient) public view override returns (uint balance_) {
  // Capture our availableFees that are waiting to be claimed from the {FeeEscrow}
  uint availableFees = managerFees() - _lastWithdrawBalance;

  // @audit Get the existing eth owed to the caller
  Position memory position = userPositions[_recipient];
  uint stakeBalance = position.ethOwed;

  // @audit Only calculate the `latestGlobalEthRewardsPerTokenX128` if totalDeposited != 0
  if (totalDeposited == 0) {
    // Get the total ETH owed to the user from their staked position, calculating the
    // latest `globalEthRewardsPerTokenX128` based on the available fees balance
    uint latestGlobalEthRewardsPerTokenX128 = globalEthRewardsPerTokenX128 + FullMath.mulDiv(
        availableFees,
        FixedPoint128.Q128,
        totalDeposited
    );

    // Calculate the stake balance based on the latest `globalEthRewardsPerTokenX128`
    stakeBalance += FullMath.mulDiv(
        latestGlobalEthRewardsPerTokenX128 - position.ethRewardsPerTokenSnapshotX128,
        position.amount,
        FixedPoint128.Q128
    );
  }

  // We then need to check if the `_recipient` is the creator of any tokens
  uint creatorBalance = pendingCreatorFees(_recipient);

  // We then need to check if the `_recipient` is the owner of the manager
  uint ownerBalance;
  if (_recipient == managerOwner) {
      ownerBalance = claimableOwnerFees();
  }

  balance_ = stakeBalance + creatorBalance + ownerBalance;
}
Previous
WhitelistPermissions.sol