Missing fund distribution when totalShares is zero leads to permanent fund loss and wrong allocation
_sync() updates accountedBalance even when totalShares = 0, marking incoming funds as accounted-for without distributing them. ~50% of those funds become permanently inaccessible and the next first-shareholder gets unfair allocation.
Description
The _sync() function in DPLTeam contract updates accountedBalance regardless of whether funds can be distributed to shareholders. When funds arrive while totalShares = 0 (no active shareholders), these funds are marked as "accounted for" but never allocated to the reward distribution system, creating both permanent fund loss and unfair distribution when shareholders are re-added.
Vulnerable Scenario:
The following steps demonstrate the dual impact of fund loss and theft:
- Initial Setup: Owner sets up shareholders (Alice: 100 shares, Bob: 50 shares, total: 150 shares)
- Normal Operation: 150 APE arrives and distributes correctly (Alice: 100 APE, Bob: 50 APE)
- Shareholder Removal: Owner temporarily removes all shareholders by setting their shares to 0 (
totalShares = 0) during team restructuring - Vulnerable Period: PixelLotteryAPE continues operating and sends 200 APE to DPLTeam contract while
totalShares = 0 - Broken Accounting:
_sync()function executes:accountedBalanceis updated to include the 200 APE, butrewardPerShareremains unchanged sincetotalShares = 0 - First Shareholder Advantage: Owner re-adds Alice first (200 shares), the "first shareholder" logic allocates
address(this).balance - totalUnclaimedProceeds, giving Alice unfair access to funds - Second Shareholder Disadvantage: Owner adds Bob (100 shares), Bob retains only his original 50 APE proceeds, missing his proportional share of the 200 APE
- Permanent Loss: Some funds become permanently inaccessible due to broken accounting synchronization
Concrete Impact from Proof of Concept:
- 200 APE sent during
totalShares = 0 - Expected fair distribution: Alice should get ~233 APE total (100 original + 133 proportional), Bob should get ~117 APE total (50 original + 67 proportional)
- Actual unfair result: Alice gets 240 APE (+7 APE surplus), Bob gets 70 APE (-47 APE deficit)
- 100 APE permanently lost and inaccessible to any shareholder
- Net effect: 50% fund loss + unfair redistribution favoring first shareholder
function _sync() internal {uint256 currentBalance = address(this).balance;if (currentBalance > accountedBalance) {uint256 delta = currentBalance - accountedBalance;if (totalShares > 0) {rewardPerShare += (delta * MULTIPLIER) / totalShares;}accountedBalance = currentBalance; // <- Always updated, even when funds not distributed}}
Impact
- Permanent fund loss: Approximately 50% of funds arriving during zero-shareholder periods become permanently inaccessible
- Economic manipulation: First shareholder added back receives unfair allocation, enabling potential insider advantage
- Broken proportional distribution: Core contract functionality compromised, violating fair share-based distribution principle
Recommendation
Only update accountedBalance when funds are actually distributed to prevent accounting discrepancies:
function _sync() internal {uint256 currentBalance = address(this).balance;if (currentBalance > accountedBalance) {uint256 delta = currentBalance - accountedBalance;if (totalShares > 0) {rewardPerShare += (delta * MULTIPLIER) / totalShares;accountedBalance = currentBalance; // Only update when distributed}// If totalShares == 0, leave accountedBalance unchanged// so funds remain "unaccounted" and will be distributed later}}
Resolution
Golden Grid: Confirmed.
Zealynx: Fixed.

