Lack of reward token segregation leads to unreliable reward accounting and potential underpayment
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.
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:
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.
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.
Recommendation
Add separate accounting for rewards:
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.
Resolution
Ample Protocol: Fixed.
Zealynx: Verified.

