Incomplete accounting in receiveFunds() leads to permanent denial of service after reward claims
receiveFunds() compares balance against totalDeposited - totalWithdrawnForStaking but ignores monthly reward deposits and reward claims, causing arithmetic underflow and permanent function failure.
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.
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 updatingtotalDeposited - Reward claims (lines 158, 191 in
claimRewardsandclaimMultipleRewards) which decrease balance and updatetotalRewardsDistributed, but this variable is never used in thereceiveFunds()calculation
Vulnerable Scenario:
-
NexumManager deposits 10,000 USDC via
depositFunds(): balance = 10,000, totalDeposited = 10,000 -
Owner deposits 5,000 USDC as monthly BTC staking rewards via
depositMonthlyRewards(5000e6): balance = 15,000, totalDeposited unchanged -
Users claim 2,000 USDC in rewards: balance = 13,000, totalDeposited unchanged
-
Someone calls
receiveFunds(): expectedBalance = 10,000, actualBalance = 13,000, newDeposit = 3,000 (incorrectly attributed) -
Users claim another 4,000 USDC: balance = 9,000, totalDeposited = 13,000
-
Anyone calls
receiveFunds(): expectedBalance = 13,000, actualBalance = 9,000, newDeposit = 9,000 - 13,000 = -4,000 → underflow → revert -
All subsequent calls to
receiveFunds()permanently revert.
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.
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 brokenreceiveFunds()function prevents automatic balance reconciliation and causes integration issues between NexumManager and TreasuryBTC.
Recommendation
Redesign the accounting system to track all balance-changing operations:
// Add state variableuint256 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;// ... rest of function}function receiveFunds() external {uint256 balance = stablecoin.balanceOf(address(this));// Calculate expected balance including all tracked operationsuint256 expectedBalance = totalDeposited+ totalRewardDeposits- totalWithdrawnForStaking- totalRewardsDistributed;if (balance > expectedBalance) {uint256 newDeposit = balance - expectedBalance;totalDeposited += newDeposit;emit FundsReceived(msg.sender, newDeposit);}}
Resolution
Nexalo: Fixed.
Zealynx: Verified. Fixed.

