F-2025-0015·design-flaw

Immutable audit funds address and missing approval mechanism leads to unreachable cleanup logic and mixed fund accounting

Fixedrafflelotteryvrf
TL;DR

auditFunds is permanently set equal to founder with no setter; cleanup logic can never execute and audit/founder/fees mix in one address with no separation.

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

Description

The auditFunds address is initialized to the same value as founder in the constructor and cannot be changed due to missing setter functions. This creates multiple issues with audit fund management and cleanup logic.

solidity
// NexumManager.sol:L145-L147 (constructor)
founder = _founder;
partner = _partner;
auditFunds = _founder; // Same as founder, permanently

During prize distribution, the contract sends separate payments to both addresses:

solidity
// NexumManager.sol:L406-L416
uint256 founderAmount = (operationsTotal * FOUNDER_SHARE) / 1000;
_safeTransfer(founder, founderAmount);
uint256 auditAmount = (operationsTotal * AUDIT_SHARE) / 1000;
_safeTransfer(auditFunds, auditAmount); // Goes to founder since auditFunds == founder
uint256 feesAmount = (operationsTotal * FEES_SHARE) / 1000;
_safeTransfer(founder, feesAmount); // Also to founder

The cleanup logic in _handleNXLExhaustion() attempts to transfer audit funds when all products become inactive:

solidity
// NexumManager.sol:L315-L322
if (allInactive && auditFunds != address(0) && auditFunds != founder) {
uint256 auditBalance = stablecoin.balanceOf(auditFunds);
if (auditBalance > 0) {
try stablecoin.transferFrom(auditFunds, founder, auditBalance) {
emit AuditFundsTransferred(founder, auditBalance);
} catch {}
}
}

Vulnerable Scenario:

  1. Contract is deployed with founder and auditFunds both pointing to the same address
  2. During lottery operations, the contract distributes funds: a. Founder receives founderAmount + feesAmount directly b. auditFunds receives auditAmount (but goes to same address as founder)
  3. All three payment streams mix in the founder's address with no way to distinguish audit funds
  4. When all products become inactive, the cleanup logic attempts to execute
  5. The condition auditFunds != founder evaluates to false since they're the same address
  6. The audit fund transfer block never executes - dead code
  7. Even if auditFunds could be changed, the transferFrom would fail because: a. It attempts to transfer from auditFunds address to founder b. auditFunds address has never approved NexumManager to spend tokens c. No mechanism exists for granting this approval
  8. No setter functions exist to update founder, partner, or auditFunds addresses post-deployment
03Section · Impact

Impact

  • The audit fund management is fundamentally broken with multiple consequences. The cleanup logic at protocol end-of-life is completely unreachable dead code since auditFunds != founder is hardcoded to false forever with no way to change either address post-deployment.

  • During normal operations, audit funds, founder operational funds, and protocol fees all mix in the same address with no accounting separation, making it impossible to track or manage audit funds independently. The design confusion is evident: the code sends audit payments TO auditFunds during distribution but then attempts to retrieve funds FROM auditFunds at exhaustion using transferFrom without any approval mechanism. This entire feature serves no purpose in its current implementation.

04Section · Recommendation

Recommendation

Use pull pattern for audit funds. Instead of pushing funds at exhaustion, allow authorized address to withdraw audit allocation:

solidity
mapping(address => uint256) public auditFundBalance;
function _distributePrizes(...) private {
// ... existing logic ...
uint256 auditAmount = (operationsTotal * AUDIT_SHARE) / 1000;
auditFundBalance[auditFunds] += auditAmount; // Track separately
// ... rest of distribution ...
}
function withdrawAuditFunds() external {
require(msg.sender == auditFunds, "Not authorized");
uint256 amount = auditFundBalance[msg.sender];
require(amount > 0, "No funds");
auditFundBalance[msg.sender] = 0;
require(stablecoin.transfer(msg.sender, amount), "Transfer failed");
}
05Section · Resolution

Resolution

Nexalo: Fixed.

Zealynx: Verified. Fixed.

Status
Fixed
F-2025-0015

oog
zealynx

Smart Contract Security Digest

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

© 2026 Zealynx