F-2025-0017·accounting-error

Insufficient available rewards will result in locked staked funds for users

Acknowledgedrafflelotteryvrf
TL;DR

The receiveFunds() accounting fails to track monthly reward deposits and reward claims, causing arithmetic underflow that locks staked funds for users when WBTC reserves are insufficient.

Severity
MEDIUM
Impact
MEDIUM
Likelihood
MEDIUM
Method
MManual review
CAT.
Complexity
MEDIUM
Exploitability
MEDIUM
02Section · Description

Description

The receiveFunds() function calculates new deposits by comparing the current stablecoin balance against expected balance based on totalDeposited and totalWithdrawnForStaking. However, this accounting formula fails to track two critical balance changes: monthly reward deposits and reward claims, causing arithmetic underflow and permanent function failure.

solidity
function receiveFunds() external {
uint256 balance = stablecoin.balanceOf(address(this));
uint256 newDeposit = balance - (totalDeposited - totalWithdrawnForStaking);
if (newDeposit > 0) {
totalDeposited += newDeposit;
emit FundsReceived(msg.sender, newDeposit);
}
}

The formula assumes: expectedBalance = totalDeposited - totalWithdrawnForStaking

However, the actual balance is affected by:

  • Monthly reward deposits (line 119-138 in depositMonthlyRewards) which increase balance without updating totalDeposited
  • Reward claims (lines 158, 191 in claimRewards and claimMultipleRewards) which decrease balance and update totalRewardsDistributed, but this variable is never used in the receiveFunds() calculation

Vulnerable Scenario:

The following steps demonstrate the issue:

  1. NexumManager deposits 10,000 USDC via depositFunds():
    • balance = 10,000
    • totalDeposited = 10,000
    • totalWithdrawnForStaking = 0
  2. Owner deposits 5,000 USDC as monthly BTC staking rewards via depositMonthlyRewards(5000e6):
    • balance = 15,000
    • totalDeposited = 10,000 (unchanged - this is the problem)
    • Monthly rewards are NOT tracked in totalDeposited
  3. Users claim 2,000 USDC in rewards:
    • balance = 13,000
    • totalDeposited = 10,000 (unchanged)
    • totalRewardsDistributed = 2,000 (tracked but not used in receiveFunds())
  4. Someone calls receiveFunds():
    • expectedBalance = 10,000 - 0 = 10,000
    • actualBalance = 13,000
    • newDeposit = 13,000 - 10,000 = 3,000
    • Function succeeds but incorrectly attributes 3,000 as new deposit
    • totalDeposited = 13,000 (now includes some of the reward pool)
  5. Users claim another 4,000 USDC in rewards:
    • balance = 9,000
    • totalDeposited = 13,000
    • totalWithdrawnForStaking = 0
  6. Anyone calls receiveFunds():
    • expectedBalance = 13,000 - 0 = 13,000
    • actualBalance = 9,000
    • newDeposit = 9,000 - 13,000 = -4,000
    • Arithmetic underflow in Solidity 0.8+ → Transaction reverts
  7. All subsequent calls to receiveFunds() permanently revert because the actual balance is now below the expected balance.

The root cause: Monthly reward deposits temporarily inflate the balance above expected levels, masking the accounting issue. Once total reward claims exceed the untracked monthly reward deposits, the actual balance falls below the calculated expected balance, causing permanent underflow.

03Section · Impact

Impact

The receiveFunds() function becomes permanently unusable after normal protocol operations involving monthly reward deposits and user claims. This creates a denial of service for the permissionless accounting mechanism that NexumManager relies on to deposit treasury funds.

While depositFunds() remains functional as an alternative, the broken receiveFunds() function prevents automatic balance reconciliation and causes integration issues between NexumManager and TreasuryBTC. The function failure also prevents proper tracking of all funds entering the contract.

04Section · Recommendation

Recommendation

Implement a pull base system allowing users to pull rewards themselves.

solidity
// Add state variable
uint256 public totalRewardDeposits;
function depositMonthlyRewards(uint256 rewardAmount) external onlyOwner {
require(rewardAmount > 0, "Amount must be > 0");
require(stablecoin.transferFrom(msg.sender, address(this), rewardAmount), "Transfer failed");
totalRewardDeposits += rewardAmount;
// ...
}
function receiveFunds() external {
uint256 balance = stablecoin.balanceOf(address(this));
uint256 expectedBalance = totalDeposited
+ totalRewardDeposits
- totalWithdrawnForStaking
- totalRewardsDistributed;
if (balance > expectedBalance) {
uint256 newDeposit = balance - expectedBalance;
totalDeposited += newDeposit;
emit FundsReceived(msg.sender, newDeposit);
}
}

This ensures the expected balance calculation accounts for all funds entering and leaving the contract, preventing underflow and maintaining accurate accounting.

05Section · Resolution

Resolution

Nexalo: Fixed.

Zealynx: The DOS vulnerability remains. Users can still be locked out of staking/unstaking when WBTC reserves are insufficient to pay their pending rewards.

F-2025-0017

oog
zealynx

Smart Contract Security Digest

Monthly exploit breakdowns, audit checklists, and DeFi security research — straight to your inbox

© 2026 Zealynx