F-2025-0001·missing-accounting-separation

Lack of reward token segregation leads to unreliable reward accounting and potential underpayment

Fixedstakingnft-boostrewards
TL;DR

StakingContract does not differentiate staked tokens from reward tokens in its balance, creating a fractional-reserve scenario in which late withdrawals may fail because earlier rewards consumed staked principal.

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

Description

The StakingContract does not differentiate between staked tokens and reward tokens in its balance. When rewards are funded, they are simply added to the contract's balance without any internal tracking:

solidity
function fundRewards(uint256 amount) external onlyOwner {
// Tokens are added to contract balance
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
// No internal tracking of rewards vs staked tokens
emit RewardsFunded(msg.sender, amount);
}

When users stake or withdraw, the contract has no way to distinguish between:

  • Tokens that were staked by users.
  • Tokens that were funded as rewards.
  • Tokens that are reserved for promised interest.
03Section · Impact

Impact

  • The contract's inability to differentiate between staked tokens and reward tokens creates a dangerous situation where the protocol might be promising rewards it cannot fulfil.
  • Since the contract balance combines both staked tokens and rewards, it becomes impossible to verify whether there are sufficient rewards to cover all promised interest. This could lead to a scenario where the contract inadvertently uses staked tokens to pay rewards, effectively creating a fractional reserve system.
  • In a high-withdrawal scenario, this could result in later users being unable to withdraw their funds, as the contract might have used their staked tokens to pay earlier users' rewards.
04Section · Recommendation

Recommendation

Add separate accounting for rewards:

solidity
uint256 private _totalRewardPool;
uint256 private _totalPromisedRewards;
function fundRewards(uint256 amount) external onlyOwner {
stakingToken.safeTransferFrom(msg.sender, address(this), amount);
_totalRewardPool += amount;
emit RewardsFunded(msg.sender, amount);
}
function stakeTokens(uint numDays, uint amount) external nonReentrant {
uint256 interest = calculateInterest(tiers[numDays], amount);
require(_totalRewardPool >= _totalPromisedRewards + interest,
"Insufficient rewards available");
_totalPromisedRewards += interest;
// ... rest of function
}

Add reward tracking in position closure, and add a view function (getRewardMetrics) that exposes available and promised rewards for transparency.

05Section · Resolution

Resolution

Ample Protocol: Fixed.

Zealynx: Verified.

Status
Fixed
F-2025-0001

oog
zealynx

Smart Contract Security Digest

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

© 2026 Zealynx