F-2025-0001·missing-accounting-separation
Lack of reward token segregation leads to unreliable reward accounting and potential underpayment
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 balancestakingToken.safeTransferFrom(msg.sender, address(this), amount);// No internal tracking of rewards vs staked tokensemit 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