Incremental totalUnclaimedProceeds funding mechanism leads to systematic accounting corruption and withdrawal failures
withdraw() resets userRewardPerShare as if the user fully withdrew, even on partial withdrawals, freezing the retained proceeds out of future reward distributions and breaking the reward tracking invariant.
Description
The withdraw(uint256 amount) function contains a flaw in its reward tracking logic that permanently corrupts future reward calculations for users who perform partial withdrawals. When a user withdraws only a portion of their proceeds, the function incorrectly resets their reward tracking index (userRewardPerShare) as if they had withdrawn all funds, while simultaneously retaining the remaining proceeds in their balance.
function withdraw(uint256 amount) external nonReentrant {// ... calculate totalProceeds including pending rewardsuint256 totalProceeds = proceeds[msg.sender] + pending;// Store remaining proceeds after partial withdrawalproceeds[msg.sender] = totalProceeds - amount;// BUG: Reset tracking index as if user withdrew everythinguserRewardPerShare[msg.sender] = rewardPerShare;}
The contract uses a Compound-style reward distribution system where userRewardPerShare[user] tracks the last synchronization point for calculating pending rewards. This index should only be fully reset when proceeds[user] == 0. By resetting it during partial withdrawals while retaining proceeds, the system breaks the fundamental invariant of the reward tracking mechanism.
Vulnerable Scenario:
The following steps help understand the issue:
- Alice has 100 APE total proceeds available for withdrawal
- Alice calls
withdraw(50)to withdraw 50 APE, leaving 50 APE in her proceeds balance - The contract sets
proceeds[Alice] = 50 APE(remaining amount) - Bug: The contract sets
userRewardPerShare[Alice] = rewardPerShare(full reset) - Contract receives new deposits, increasing the global
rewardPerShare - Alice's future reward calculations only consider rewards earned after step 4
- Result: Alice's retained 50 APE never participates in future reward distributions
Impact
This vulnerability causes users who perform partial withdrawals to permanently lose the ability to earn future rewards on their retained proceeds. The retained funds become "frozen" in the reward system, unable to participate in subsequent reward distributions. The impact compounds over time as new deposits flow into the contract, with affected users missing out on proportional rewards that should apply to their retained balances. This represents a fundamental breakdown of the reward distribution system for any user who doesn't withdraw their full balance in a single transaction.
Recommendation
Fix the reward tracking logic to properly handle partial withdrawals by preventing index reset during partial withdrawals.
function withdraw(uint256 amount) external nonReentrant {// ... existing logic ...proceeds[msg.sender] = totalProceeds - amount;// Only reset index if user has no remaining proceedsif (proceeds[msg.sender] == 0) {userRewardPerShare[msg.sender] = rewardPerShare;}// If proceeds remain, keep the existing index to maintain proper tracking}
In this way, it maintains the intended functionality while fixing the core accounting flaw.
Resolution
Golden Grid: Confirmed. A test was implemented and it showed that a fix included on a previous issue, fixed this vulnerability as well.
Zealynx: Fixed. Made sure that the issue was present on commit under scope and then fixed as per dev team's test results.

